Как я добавил HEIF, HEIC и AVIF в Android-ImageMagick7

от автора

Моё приложение 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 = truefalse в 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)

Теперь нужно было:

  1. AVIF — контейнер HEIF с AV1-кодеком внутри. Декодер — dav1d от VideoLAN (быстрый, на C, с NEON-оптимизациями).

  2. Запись HEIC — нужен энкодер x265 (HEVC/H.265).

  3. Запись 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 делегатами).


Ссылки

ссылка на оригинал статьи https://habr.com/ru/articles/1062000/