Моё приложение Image Converter конвертирует изображения из любых форматов в любые. В основе — ImageMagick 7, портированный под Android через NDK-сборку.
Базовая версия ImageMagick для Android умела JPEG, PNG, WebP, TIFF. Но HEIF, HEIC, AVIF — нет. А это современные форматы, которые используют камеры iPhone (HEIC), Google Photos (AVIF), и в которые сжимают изображения всё чаще.
Внедрение шло в два этапа.
Этап 1: HEIF/HEVC (чтение)
Задача — чтобы ImageMagick мог читать HEIC/HEIF. Для этого нужна цепочка:
ImageMagick → coders/heic.c → libheif → libde265 (HEVC decoder)
libde265 — декодер HEVC (H.265), написан на C++. Собирается как обычная ndk-build статическая библиотека.
libheif — библиотека для парсинга HEIF/ISOBMFF контейнеров. Тоже C++, но тут начались проблемы.
Проблема №1: плагины не регистрируются
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.
Проблема №2: race condition конструктора
В coders/heic.c был добавлен конструктор:
__attribute__((constructor))static void InitializeHEIF(void){ heif_init(NULL);}
который вызывал heif_init() при загрузке .so. Но из-за гонки с C++ статическим инициализатором libheif плагины либо не регистрировались, либо регистрировались дважды.
Решение: убрать конструктор полностью. heif_init() вызывается в RegisterHEICImage() — этого достаточно.
Проблема №3: strerror_r на Android
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 работала.
Этап 2: AVIF (AV1) и запись HEIC (x265 + libaom)
Теперь нужно было:
-
AVIF — контейнер HEIF с AV1-кодеком внутри. Декодер — dav1d от VideoLAN (быстрый, на C, с NEON-оптимизациями).
-
Запись HEIC — нужен энкодер x265 (HEVC/H.265).
-
Запись AVIF — нужен энкодер libaom (AV1 от Google).
dav1d: сборка через Meson
dav1d собирается через Meson, не через ndk-build. Пришлось собирать его отдельно и подкладывать .a файл. Для arm64-v8a — с NEON-ассемблером, для x86_64 — без ASM (на Windows нет NASM).
x265: CMake + патчи
x265 собирается через CMake, но:
-
Устаревшие
cmake_policyиcmake_minimum_required— пришлось обновить на 3.5 и 3.10 -
ASM-код несовместим с Clang integrated assembler на ARM64 —
-DENABLE_ASSEMBLY=OFF -
x265_config.hгенерируется CMake и его надо копировать вinclude/вручную
Проблема: plugin_info — duplicate symbols
libheif содержит несколько .cc файлов, каждый из которых может определить глобальный heif_plugin_info plugin_info. При статической линковке — duplicate symbol. Ошибка появляется, только когда добавляешь больше одного плагина (libde265 + dav1d + x265).
Решение: добавить && ENABLE_PLUGIN_LOADING к #if guard у каждого plugin_info, потому что при статической линковке динамическая загрузка плагинов не нужна:
// Было:#if PLUGIN_LIBDE265heif_plugin_info plugin_info {// Стало:#if PLUGIN_LIBDE265 && ENABLE_PLUGIN_LOADINGheif_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
ссылка на оригинал статьи https://habr.com/ru/articles/1062000/