HEIF, HEIC и AVIF на Android через ImageMagick: хроника внедрения в два этапа. Без лишней воды — только проблемы и их решения. Читать далее
Моё приложение Image Converter конвертирует изображения из любых форматов в любые. В основе — ImageMagick 7, портированный под Android через NDK-сборку.
Базовая версия ImageMagick для Android умела JPEG, PNG, WebP, TIFF. Но HEIF, HEIC, AVIF — нет. А это современные форматы, которые используют камеры iPhone (HEIC), Google Photos (AVIF), и в которые сжимают изображения всё чаще.
Внедрение шло в два этапа.
Задача — чтобы ImageMagick мог читать HEIC/HEIF. Для этого нужна цепочка:
ImageMagick → coders/heic.c → libheif → libde265 (HEVC decoder)
libde265 — декодер HEVC (H.265), написан на C++. Собирается как обычная ndk-build статическая библиотека.
libheif — библиотека для парсинга HEIF/ISOBMFF контейнеров. Тоже C++, но тут начались проблемы.
libheif использует C++ статическую инициализацию для регистрации декодеров. В plugin_registry.cc есть глобальный объект Register_Default_Plugins, который должен вызываться при загрузке .so. Но на Android порядок статической инициализации не гарантирован.
Симптом: heif_init() думает «плагины уже зарегистрированы» (потому что default_plugins_registered = true), хотя на самом деле Register_Default_Plugins ещё не отработал. В результате — ошибка:
No decoding plugin installed for this compression format (11.6003)
Решение: поменять default_plugins_registered = true → false в init.cc. Теперь heif_init() всегда регистрирует плагины принудительно, независимо от C++ static init.
В coders/heic.c был добавлен конструктор:
__attribute__((constructor))
static void InitializeHEIF(void)
{
heif_init(NULL);
}
который вызывал heif_init() при загрузке .so. Но из-за гонки с C++ статическим инициализатором libheif плагины либо не регистрировались, либо регистрировались дважды.
Решение: убрать конструктор полностью. heif_init() вызывается в RegisterHEICImage() — этого достаточно.
NDK-версия strerror_r возвращает int, а код ImageMagick ожидает char* (GNU-вариант). Компилятор ругается, сборка падает.
Решение: добавить #elif defined(__ANDROID__) в exception.c:
#elif defined(__ANDROID__)
{
char buffer[255];
if (strerror_r(error, buffer, sizeof(buffer)) != 0)
*severity = FatalError;
(void) CopyMagickString(message, buffer, length);
}
Проблема №4: линковка через --whole-archiveПри статической линковке линкер выбрасывает «неиспользуемые» символы. libheif тянет libde265, но если не сказать линкеру --whole-archive, символы de265_* не попадут в финальный .so.
Решение: использовать LOCAL_WHOLE_STATIC_LIBRARIES вместо LOCAL_STATIC_LIBRARIES в libmagickcore-7.mk:
ifeq ($(LIBHEIF_ENABLED),true)
LOCAL_CFLAGS += -DMAGICKCORE_HEIC_DELEGATE=1 -DHEIC_DELEGATE=1
LOCAL_WHOLE_STATIC_LIBRARIES += libheif libde265 libdav1d libx265 libaom
LOCAL_LDLIBS += -ldl -lc++
endif
После этих исправлений HEIC/HEIF читались — конвертация HEIF → JPEG/PNG работала.
Теперь нужно было:
AVIF — контейнер HEIF с AV1-кодеком внутри. Декодер — dav1d от VideoLAN (быстрый, на C, с NEON-оптимизациями).
Запись HEIC — нужен энкодер x265 (HEVC/H.265).
Запись AVIF — нужен энкодер libaom (AV1 от Google).
dav1d собирается через Meson, не через ndk-build. Пришлось собирать его отдельно и подкладывать .a файл. Для arm64-v8a — с NEON-ассемблером, для x86_64 — без ASM (на Windows нет NASM).
x265 собирается через CMake, но:
Устаревшие cmake_policy и cmake_minimum_required — пришлось обновить на 3.5 и 3.10
ASM-код несовместим с Clang integrated assembler на ARM64 — -DENABLE_ASSEMBLY=OFF
x265_config.h генерируется CMake и его надо копировать в include/ вручную
libheif содержит несколько .cc файлов, каждый из которых может определить глобальный heif_plugin_info plugin_info. При статической линковке — duplicate symbol. Ошибка появляется, только когда добавляешь больше одного плагина (libde265 + dav1d + x265).
Решение: добавить && ENABLE_PLUGIN_LOADING к #if guard у каждого plugin_info, потому что при статической линковке динамическая загрузка плагинов не нужна:
// Было:
#if PLUGIN_LIBDE265
heif_plugin_info plugin_info {
// Стало:
#if PLUGIN_LIBDE265 && ENABLE_PLUGIN_LOADING
heif_plugin_info plugin_info {
В Image Converter одна кнопка конвертации обрабатывает сразу HEIF, HEIC, TIFF и AVIF. Пользователь выбирает входной файл, выбирает выходной формат — и ImageMagick делает всё остальное.
Технически это выглядит так:
JNI (app) → MagickWand API → coders/heic.c → libheif (C++ API)
→ libde265 (HEVC decode)
→ dav1d (AV1 decode)
→ x265 (HEVC encode)
→ libaom (AV1 encode)
Вся эта цепочка — внутри одного libmagickcore-7.so.
Формат | Чтение | Запись |
|---|---|---|
HEIC/HEIF | ✅ libde265 | ✅ x265 |
AVIF | ✅ dav1d | ✅ libaom |
JPEG/PNG/WEBP/TIFF | ✅ (было) | ✅ (было) |
Размер libmagickcore-7.so для arm64-v8a: ~17 MB (с HEIF/AVIF делегатами).
Исходники на GitHub: AIgrator/Android-ImageMagick7
Приложение на Google Play: Image Converter
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Тегирование людей на изображениях и Генерация заголовков для видеороликов | 0 | 5.22 | 31-05-2026 |
| 2 | [Перевод] Я создаю защищённый USB-накопитель, который скрывает часть своего пространства | 0 | 9.12 | 22-07-2026 |
| 3 | Названы смартфоны основных брендов, которые получат Android 17 | 0 | 7 | 20-06-2026 |
| 4 | Настройка AI-агентов для ускорения бизнес процессов компании | 0 | 7 | 03-07-2026 |
| 5 | Google начала сохранять ваши изображения и аудио для обучения ИИ — вот как это отключить | 0 | 7 | 23-06-2026 |
| 6 | Systweak PDF Editor for Android Adds PDF Compression | 2 | 6 | 13-04-2026 |
| 7 | Snapseed Camera on Android adds grid lines & levels | 0 | 5 | 01-07-2026 |
| 8 | Adobe DNG Converter | 0 | 5 | 06-07-2026 |
| 9 | Видеомонтаж в blender, бесплатный и надёжный | 5 | 7 | 10-07-2026 |
| 10 | Удалять ненужные фото и видео в смартфоне — важнее, чем кажется. Это забота об экологии | 0 | 0 | 11-06-2021 |