PNG из Bitmap с переносом альфа-канала. И наоборот

bmp-to-png

Стандартными средствами Delphi можно создать PNG из Bitmap, но альфа-канал при этом теряется. В этой статье разберём, как корректно конвертировать TBitmap в TPngImage с сохранением прозрачности и почему важно учитывать AlphaFormat.

PNG из Bitmap

В 2022 году я написал функцию, которая решала эту задачу — копировала альфа-байты из 32-битного битмапа в TPngImage. Функция работала, но содержала проблемы: ручную арифметику указателей, несовместимую с Win64, и полное игнорирование AlphaFormat, из-за чего при определённых условиях вокруг полупрозрачных областей появлялась характерная серая окантовка. Разберём, в чём причина, и напишем исправленную версию.

Первая версия функции

Функция рабочая, но в ряде случаев могла выдать PNG с серой каймой вокруг изображения:

Серая окантовка вокруг кота

Эволюция функции

Пока разбирался с глюком PNG, выяснил, как TPngImage формирует прозрачность при отрисовке. Это объяснило причину серой окантовки и потребовало переработки функции.

Что изменилось и почему

Принудительная установка afIgnored

Это ключевое исправление. Свойство TBitmap.AlphaFormat определяет, как GDI и VCL интерпретируют содержимое альфа-канала:

ЗначениеСмысл
afIgnoredАльфа-байт хранится как есть, цвета — straight (не домножены)
afDefinedАльфа учитывается при отрисовке; GDI хранит цвета в формате premultiplied alpha
afPremultipliedТо же, что afDefined, данные уже premultiplied

Формат premultiplied alpha означает, что каждый цветовой компонент пикселя домножен на значение альфы:

TPngImage, напротив, работает исключительно со straight alpha: цветовые компоненты хранятся в исходном виде, а альфа-канал задаётся отдельно. При отрисовке TPngImage сам формирует прозрачность, комбинируя цвет и альфу.

Если скопировать premultiplied-данные в TPngImage без обратного преобразования, произойдёт двойное применение альфы: сначала цвета уже приглушены домножением, а затем TPngImage при отрисовке применит альфу ещё раз. Визуально это проявляется как серая или тёмная окантовка вокруг полупрозрачных краёв и общее затемнение полупрозрачных областей.

Установка ABitmap.AlphaFormat := afIgnored перед вызовом Result.Assign(ABitmap) гарантирует, что VCL передаст цветовые данные в straight-формате. После копирования исходное значение AlphaFormat восстанавливается в блоке finally, чтобы не нарушить дальнейшую работу с битмапом.

Важно: переключение AlphaFormat туда и обратно может исказить пиксельные данные битмапа (VCL выполняет внутренние преобразования при смене этого свойства). Если битмап после вызова BitmapToPNG больше не нужен, его необходимо освободить. Если нужен, то безопаснее передавать в функцию копию.

Нет серой окантовки вокруг кота

Линейный обход памяти вместо ручной арифметики

В 32-битном DIB каждый пиксель занимает ровно 4 байта (SizeOf(TRGBQuad)). Строка из W пикселей занимает W × 4 байт. Выравнивание строк в Windows DIB происходит по границе 4 байт (DWORD), а W × 4 всегда кратно четырём при любом значении W. Это означает, что между строками нет «дырок» (padding-байтов) и все пиксели расположены в памяти непрерывно.

Подробно об этом рассказано в статье TBitmap.ScanLine.

Стандартный формат хранения DIB — bottom-up: строка с индексом 0 (верхняя в изображении) находится в памяти последней, а строка Height-1 (нижняя) — первой, по наименьшему адресу. Вызов ScanLine[Height-1] возвращает указатель на самое начало пиксельного буфера.

Благодаря этим двум фактам весь блок пикселей можно обойти одним указателем P, последовательно инкрементируя его через Inc(P):

Указатель P имеет тип PRGBQuad, поэтому Inc(P) сдвигает его ровно на SizeOf(TRGBQuad) = 4 байта, к следующему пикселю. Никакой дополнительной арифметики не требуется.

Сравните со старой версией, где адрес каждой строки вычислялся вручную:

Приведение указателя к Integer обрезает старшие 4 байта адреса в 64-битном процессе, что приводит к access violation или повреждению памяти. Новая версия не содержит подобной арифметики и корректно работает как в 32-битных, так и в 64-битных приложениях.

Перенос альфа-канала из PNG в Bitmap

Обратная задача — взять альфа-канал из TPngImage и записать его в 32-битный TBitmap. Это полезно, когда нужно наложить прозрачность одного изображения на содержимое другого. Функция принимает PNG-источник альфы и битмап-приёмник; размеры могут не совпадать — копируется область пересечения.

В отличие от BitmapToPNG, здесь мы не используем линейный обход одним указателем, а запрашиваем ScanLine[Y] на каждой строке. Причина в том, что размеры PNG и битмапа — независимые параметры. Если ширина PNG меньше ширины битмапа, внутренний цикл обработает только W = Min(AImage.Width, ABitmap.Width) пикселей, но указатель, бегущий линейно по памяти битмапа, не узнает, что строка на самом деле длиннее. На следующей итерации он продолжит с того места, где остановился — со смещением внутрь той же строки, а не с начала следующей. Альфа-канал «поедет» вправо с каждой строкой.

В BitmapToPNG этой проблемы нет: PNG создаётся из того же битмапа, размеры всегда совпадают, и линейный обход корректен. Здесь же ScanLine[Y] гарантирует правильный адрес начала строки независимо от соотношения размеров.

Переносить альфа-канал можно на что угодно. Например, у нас есть PNG с чуть размытым по Гауссу содержимым.

Рис.1. PNG с альфа-каналом

И некий абстрактный фон. Допустим, такой jpg.

Рис.2. JPG с абстрактным содержимым

Теперь, если преобразовать JPG с рисунка 2 в битмап и перенести в этот битмап альфа-канал из рисунка 1, то получится следующее:

Рис.3. Перенос альфа-канала в битмап

Получить Bitmap из TGraphic

Битмап в графике нужен всегда. Для обработки и анализа изображения. Для переноса данных из одного формата в другой. Просто для отрисовки. Все графические форматы рисуют не себя, а битмап, который формируется на основании данных, хранящихся в нем.

Способ 1: Рисуем

Не всегда графический формат согласен отдать свой внутренний битмап. Использование Assign также может не дать желаемого результата. Поэтому будем просто рисовать на битмапе искомый TGraphic.

Почему закомментирован вызов SetPngAlphaToBitmap. Потому что не всегда нужно переносить альфа-канал. Если это необходимо, вполне можно вызвать после LoadBitmapFromGraphic.

Для работы с альфа-каналом у нас есть отдельная функция.

Способ 2: Альфа-канал

Метод Assign для потомков TGraphic вещь универсальная. В силу этого не всегда делает то, что хочется. Поэтому чуть модифицируем и дополним.

Почему закомментирована последняя строка. Свойство AlphaFormat следует устанавливать после всех операций с альфа-байтами. После вызова функции LoadBitmapFromGraphic, возможно, захочется еще что-то поделать с битмапом. Если свойство AlphaFormat уже будет выставлено, результаты будут крайне забавными.

Что тут происходит. Дело в том, что достаточно часто возникает ситуация, когда мы применили Assign, сделали PixelFormat = pf32bit и выставили AlphaFormat = afDefined, и в результате при отрисовке видим… пустоту! Чистый лист. Нетронутый холст. Такое ощущение, что произошла ошибка.

Но ошибки нет. Просто у нас везде альфа-канал равен 0. Непрозрачная исходная картинка, альфа-канала в ней нет. Мы перенесли данные и, заказав 32 бита, получили в 4-м байте пикселя 0. То есть прозрачный пиксель. И рисует все правильно — просто битмап абсолютно прозрачный.

Поэтому делаем проверку. Если у нас все альфа-байты в битмапе нулевые — это однозначно непрозрачная картинка. Если это так, выставляем все альфа-байты в 255.

Пример использования

У нас есть два TImage. Image1 отвечает за прозрачный PNG, Image2 — за фон. Открываем графические файлы и вставляем из буфера как было описано ранее.

Полный текст лучше посмотреть в исходниках по ссылке ниже. Что тут происходит. Если в Image1 находится TPngImage, то используем Assign. Если что-то другое, а при вставке из буфера нам прилетает TBitmap, используем LoadBitmapFromGraphic из способа 2.

Обработчик события OnPaint компонента pb: TPaintBox выглядит следующим образом:

Здесь мы воспользовались функцией LoadBitmapFromGraphic из способа 1. Мы преобразовали TGraphic, который находится в Image2, в Bitmap. Затем сделали его полностью прозрачным функцией SetBitmapAlpha. Потом перенесли альфа-канал из ранее подготовленного PNG функцией SetPngAlphaToBitmap. И только после всех манипуляций с альфа-каналом установили формат AlphaFormat = afDefined.

В демо-приложении реализован режим сравнения: можно переключаться между отрисовкой исходного PNG, результатом старой функции BitmapToPNG (с двумя параметрами) и новой (с одним), чтобы увидеть разницу — в частности, серую окантовку при premultiplied-данных.


Скачать

Друзья, спасибо за внимание!

Исходник (zip) 2.16 Мб. Delphi XE 7, 13.0 Florence

Исполняемый файл (zip) 2.75 Мб.

Примечание: в 64-битной сборке (Delphi 13.0 Florence) выбор старого метода конвертации вызовет Access Violation — именно тот баг с PByte(Integer(d)), описанный в статье. Исключение штатно перехватывается блоком try…except. Старая функция намеренно оставлена без исправлений для наглядной демонстрации проблемы.


5 7 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

2 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Олег

Роман, весьма полезная статья! Благодарю, пригодится в хозяйстве.