Clipboard в Delphi: Изображение без потери альфа-канала

Для тестирования и изучения часто использую картинки из браузера. Но очень часто бывает так, что скопировал, вставил и вместо прозрачности вижу чёрный фон. А мне как раз нужна прозрачность, альфа-канал. Давайте разберёмся, почему так происходит и как это исправить.

Проблема не в изображении и не в браузере. Проблема в том, что VCL берёт из буфера обмена не тот формат. А тот формат лежит рядом, нетронутый. В буфере обмена Windows одновременно лежат несколько представлений одного и того же изображения. И среди них находится полноценный PNG с альфа-каналом. Но Delphi его не видит. Почему?

Как VCL работает с изображением в буфере обмена

Типичный код вставки изображения из буфера обмена в Delphi выглядит так:

Или ещё короче:

Просто, элегантно, но именно тут мы теряем альфа-канал. Чтобы понять почему, нужно заглянуть внутрь.

Что происходит при вызове TPicture.Assign

TPicture не работает с буфером обмена напрямую. Он опирается на реестр зарегистрированных графических форматов — внутренний список VCL, который связывает форматы буфера обмена с классами-наследниками TGraphic.

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

Когда вызывается TPicture.Assign(Clipboard), VCL перебирает этот список и ищет первый формат, который доступен в буфере. И если в буфере растр, то всегда находится CF_BITMAP, потому что другого Delphi не знает.

CF_BITMAP: формат без альфы

CF_BITMAP — это стандартный растровый формат буфера обмена, и именно его забирает VCL. Проблема в том, что CF_BITMAP представляет собой хэндл GDI-объекта (HBITMAP), а GDI исторически не знает, что такое альфа-канал. Четвёртый байт в 32-битном пикселе для GDI — просто мусор, пустое место для выравнивания. Но откуда CF_BITMAP вообще берётся, если приложение-источник его туда не положило? Тут вступает в игру механизм синтеза.

Как Windows синтезирует форматы

Когда приложение кладёт в буфер обмена изображение, оно обычно предоставляет один-два формата. Остальные Windows синтезирует автоматически. Например, если источник положил CF_DIB, Windows сам создаст CF_BITMAP. И наоборот.

Цепочка для VCL выглядит так:

  1. VCL просит у буфера CF_BITMAP;
  2. Если CF_BITMAP нет напрямую, Windows синтезирует его из CF_DIB или CF_DIBV5;
  3. Создаётся HBITMAP, GDI-объект, привязанный к экранному контексту;
  4. VCL оборачивает его в TBitmap.

Windows синтезирует CF_BITMAP из CF_DIB, сохраняя битность источника. Если приложение положило в буфер 32-битный DIB, то HBITMAP тоже будет 32-битный, и все четыре байта каждого пикселя скопируются как есть, включая четвёртый.

С точки зрения GDI четвёртый байт — это просто padding, выравнивание. GDI им не пользуется. Но физически он на месте. И если источник записал туда осмысленные значения альфа-канала, они доедут до TBitmap в целости.

Вот только VCL об этом не знает. После Assign(Clipboard) у TBitmap стоит AlphaFormat = afIgnored. Это означает, что при отрисовке четвёртый байт игнорируется. Картинка рисуется без прозрачности, но она хотя бы видна.

Можно ли спасти альфу

Можно попробовать. После вставки проходим по пикселям через ScanLine и проверяем четвёртый байт:

Если нашли ненулевые значения, выставляем AlphaFormat := afDefined, и VCL начинает рисовать через AlphaBlend. Прозрачность работает.

Но результат сильно зависит от источника

Копируем из Paint.NET: Наш метод, описанный выше, работает, альфу определяем, получаем прозрачный фон.

Копируем из браузера (Chrome, Firefox, Edge): Пытаемся найти альфу, но альфа-байты все до единого нули. Не потому что изображение прозрачное, а потому что браузер просто не заполняет альфа-канал в этом формате. Он рассчитывает на то, что умный потребитель возьмёт PNG. Высказывание явно не в пользу Delphi.

Что мы видим после вставки: 32 бита на пиксель, цвета на месте, альфа сплошные нули. Наша проверка не находит ненулевых значений и AlphaFormat остаётся afIgnored. Картинка отрисовывается без прозрачности. Если вслепую выставим afDefined, то получим полностью невидимый прямоугольник, потому что альфа = 0 означает «полностью прозрачный».

И никакими манипуляциями с TBitmap это не исправить. Данных альфа-канала в CF_DIB физически нет. Их негде взять.

Получается, что стандартный путь VCL через CF_BITMAP — это лотерея. Иногда альфа доезжает, иногда нет. Зависит не от нашего кода, а от того, что источник положил в буфер.

Что на самом деле лежит в буфере

Картинка в буфере обмена находится не в единственном экземпляре. Это контейнер с множеством представлений одних и тех же данных. Когда браузер копирует изображение, он кладёт туда сразу несколько форматов, и среди них почти всегда есть полноценный PNG с альфа-каналом. VCL его не ищет, потому что формат PNG не зарегистрирован в списке TPicture.

Чтобы убедиться в этом, давайте напишем утилиту, которая покажет всё содержимое буфера.

Утилита: Содержимое буфера обмена

Напишем простую утилиту-просмотрщик буфера обмена. Она покажет все форматы, которые доступны в данный момент, их размер и тип. Интерфейс, служебные вещи описывать не буду, их можно скачать и подсмотреть в исходнике.

Имена стандартных форматов

Windows определяет 17 стандартных форматов буфера обмена с ID от 1 до 17. У них нет текстовых имён, которые можно получить через API, только числовые константы. Поэтому заведём табличку соответствий:

Для кастомных форматов (например, PNG или HTML Format) имя можно получить через GetClipboardFormatName.

Определяем размер данных

Не все форматы хранят данные как блок глобальной памяти. Некоторые возвращают GDI-хэндлы: для CF_BITMAP это HBITMAP, для CF_ENHMETAFILE — HENHMETAFILE, для CF_PALETTE — это HPALETTE. Вызов GlobalSize на таком хэндле приведёт к Access Violation.

Поэтому оборачиваем получение размера в безопасную функцию:

Примечание: В модуле (который представлен в листинге) мы не оборачиваем GlobalSize, потому что к этому моменту хэндл уже гарантированно валиден после успешного GetClipboardData. Здесь же нам нужно безопасное получение размера, потому что мы перебираем все форматы, содержащиеся в буфере обмена.

Перечисляем форматы

Вот основная процедура. Открываем буфер, перебираем все форматы через EnumClipboardFormats, для каждого определяем имя, размер и тип:

Запускаем и смотрим

Копируем картинку с прозрачностью в браузере и жмём кнопку. Вот что мы видим:

Важный момент: EnumClipboardFormats перечисляет форматы в определённом порядке. Сначала идут те, которые приложение-источник реально положило в буфер. Затем те, которые Windows синтезировал автоматически. Синтез работает по группам: если источник положил CF_DIB, Windows автоматически создаёт CF_BITMAP и CF_DIBV5. И наоборот.

В нашем примере браузер положил кучу всего вместе с картинкой. Нас интересуют форматы: PNG и CF_DIBV5. CF_DIB и CF_BITMAP — синтезированы Windows.

Главное, что мы видим полноценный PNG-файл, возможно с альфа-каналом, размером чуть больше 383 KB. Именно его VCL проигнорирует, схватив вместо этого синтезированный CF_BITMAP без прозрачности.

Сейчас мы достанем PNG оттуда сами.

Достаём PNG из буфера обмена

Формат PNG — это кастомный формат буфера обмена. Он не входит в список стандартных (1–17), а регистрируется приложением-источником через RegisterClipboardFormat(‘PNG’). У него нет фиксированного ID, он будет разным при каждой загрузке Windows. Поэтому первым делом нам нужно узнать его ID:

Несмотря на слово «Register», функция не создаёт новый формат, если он уже зарегистрирован. Она просто возвращает его ID. Если хром его зарегистрировал ранее, мы получим тот же идентификатор, что и у браузера.

Извлекаем данные

Данные кастомного формата лежат в буфере как блок глобальной памяти. Это буквально содержимое PNG-файла, байт в байт. Нужно его скопировать в поток и скормить TPngImage:

Что здесь происходит

Цепочка вызовов повторяет стандартный паттерн работы с буфером обмена через WinAPI:

  1. IsClipboardFormatAvailable — быстрая проверка без открытия буфера;
  2. OpenClipboard(0) — захватываем буфер (0 = без привязки к окну);
  3. GetClipboardData — получаем хэндл блока памяти;
  4. GlobalLock — превращаем хэндл в указатель на данные;
  5. Копируем в TMemoryStream, загружаем в TPngImage;
  6. GlobalUnlock + CloseClipboard — обязательно освобождаем.

Можно открыть Vcl.Clipbrd и подсмотреть, как строится работа с буфером обмена в Delphi. Там будет ровно тот же шаблон.

Важно: данные из GetClipboardData нельзя освобождать, они принадлежат буферу обмена. Мы их только читаем и копируем.

Тонкость: TPngImage косячит при масштабировании

В предыдущей статье мы разобрали случай, когда TPngImage отображает себя с некоторым смещением при масштабировании. В той же статье сделали вывод, что самое простое — это получать из него TBitmap и отображать уже его.

Поэтому будем конвертировать TPngImage в TBitmap перед отображением:

TPngImage.AssignTo создаёт 32-битный TBitmap с AlphaFormat := afDefined. При этом альфа-канал сохраняется, а масштабирование берёт на себя WinAPI.

Подключаем к интерфейсу

При клике на строку PNG в нашем ListView вызываем извлечение и показываем результат:

Результат

Копируем PNG с прозрачностью из браузера, жмём «Refresh», кликаем на строку PNG и видим картинку с альфа-каналом. Прозрачные области отображаются корректно, без чёрного фона.

Всё, что нужно было, это попросить у буфера правильный формат.

В следующем разделе добавим запасной вариант — извлечение через CF_DIBV5, на случай если источник не положил PNG.

Достаём CF_DIBV5 из буфера обмена

Не все приложения кладут в буфер формат PNG. Но многие кладут CF_DIBV5. Это расширенный формат DIB с заголовком BITMAPV5HEADER, который умеет описывать 32-битные пиксели с альфа-каналом. VCL его не обрабатывает, но мы можем разобрать вручную.

Что внутри CF_DIBV5

Данные CF_DIBV5 представляют собой непрерывный блок памяти. Сначала идёт заголовок BITMAPV5HEADER размером 124 байта (SizeOf(BITMAPV5HEADER)), сразу за ним — пиксельные данные.

Формат пиксельных данных зависит от параметра структуры bV5BitCount. Нас интересует только 32-битный вариант — именно он содержит альфа-канал. При bV5BitCount = 32 каждый пиксель занимает 4 байта: Blue, Green, Red, Alpha. Знакомая структура, если работали с TBitmap.PixelFormat = pf32bit. Всё остальное мы отбрасываем — без альфа-канала нет смысла возиться с CF_DIBV5, проще отдать картинку стандартному обработчику CF_DIB.

Проблема с альфой

Как мы выяснили ранее, недостающие форматы Windows синтезирует автоматически. Помимо PNG, браузер кладёт изображение в одном из растровых форматов (CF_DIB или CF_DIBV5), остальные Windows синтезирует автоматически. Но при синтезе альфа-канал теряется — все альфа-байты равны нулю.

Если мы установим AlphaFormat := afDefined, то альфа = 0 будет означать «полностью прозрачный» и мы получим полностью прозрачную, то есть абсолютно невидимую, картинку.

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

Порядок строк: bottom-up vs top-down

Ещё один подводный камень. DIB хранит строки пикселей в порядке снизу вверх (bottom-up) — так исторически сложилось. Признак — положительное значение bV5Height. Если bV5Height отрицательное — строки идут сверху вниз (top-down).

TBitmap.ScanLine[0] — это всегда верхняя строка, как бы данные внутри битмапа на самом деле не располагались. Поэтому при чтении bottom-up нужно перевернуть порядок.

Код извлечения

Ключевые моменты

Проверка bV5BitCount <> 32: нас интересует только 32-битный формат, где есть место для альфа-байта. 24-битный CF_DIBV5 обрабатывать смысла нет, потому что там альфы точно нет.

Проверка bV5Compression > BI_BITFIELDS: мы поддерживаем BI_RGB (0) и BI_BITFIELDS (3). Экзотические варианты вроде JPEG/PNG-сжатия внутри DIB пропускаем.

Указатель на пиксели: PByte(DataPtr) + Header^.bV5Size. Заголовок BITMAPV5HEADER имеет фиксированный размер 124 байта, записанный в поле bV5Size. Пиксели идут сразу за ним.

Проверка альфы: один проход по всем пикселям. Как только нашли хоть одно ненулевое значение, то выставляем HasRealAlpha := True, и дальше можно не проверять (но копировать строки всё равно нужно).

Убеждаемся, что браузер кладёт CF_DIBV5 без альфы. Поэтому картинки, скопированные из браузера и вставленные с помощью VCL, не содержат данные альфа-канала.

Теперь мы можем убедиться, что Paint.NET укладывает в буфер обмена CF_DIBV5 с честным альфа-каналом, который синтезируется потом в CF_BITMAP с переносом и 4-го байта прозрачности.

В следующем разделе добавим последний рубеж обороны — fallback через стандартный TPicture.Assign, чтобы хоть что-то показать, даже если ни PNG, ни CF_DIBV5 не сработали.

Последний рубеж: TPicture.Assign

Если в буфере нет ни PNG, ни CF_DIBV5 с настоящей альфой, это ещё не повод сдаваться. В буфере может лежать обычный CF_BITMAP, метафайл или что-то ещё, что VCL умеет обрабатывать штатно. Пусть без прозрачности — но хотя бы картинка будет.

Fallback через TPicture

Единственная тонкость — нельзя вернуть Pic.Graphic напрямую. Объект TPicture владеет своим Graphic и уничтожит его при Pic.Free. Поэтому создаём копию того же класса через TGraphicClass(Pic.Graphic.ClassType).Create и делаем Assign.

Собираем каскад целиком

Теперь у нас три метода извлечения, выстроенных по приоритету — от лучшего к худшему:

  1. PNG: полный альфа-канал, идеальное качество
  2. CF_DIBV5: альфа возможна, если источник её заполнил
  3. TPicture: стандартный путь VCL, без альфы, но хоть что-то

Обновляем обработчик выбора в ListView:

Так сразу видно — удалось ли сохранить прозрачность, и каким способом.

Готовый модуль: IP76.Imaging.Clipbrd

Всё, что мы разобрали по частям: каскадную загрузку PNG — CF_DIBV5 — VCL, ручной разбор альфа-канала, проверку пустой альфы, собрал в один модуль. Подключаете к проекту и заменяете стандартную вставку одной функцией:

TryLoadGraphicFromClipboard сама пробует форматы в порядке приоритета:

  1. PNG: полный альфа-канал, лучший вариант;
  2. CF_DIBV5: ручной разбор 32-битных пикселей, альфа сохраняется, если источник её заполнил;
  3. VCL fallback: стандартный TPicture.Assign(Clipboard), без альфы, но хоть что-то.

Функция возвращает TGraphic, который должен освободить вызывающий. На практике сейчас это всегда TBitmap, и был соблазн так и написать в сигнатуре. Но оставил TGraphic по нескольким причинам. Во-первых, симметрия: TrySaveGraphicToClipboard (следующий раздел) принимает TGraphic, логично, чтобы загрузка возвращала тот же тип. Во-вторых, опыт подсказывает, что ограничиваться TBitmap не стоит. Завтра может появиться формат, для которого разумнее вернуть другой класс (например, TMetafile из CF_ENHMETAFILE), и сигнатуру не придётся ломать.

Если нужен контроль, можно вызывать каждый уровень отдельно: TryLoadPNG, TryLoadDIBV5, TryLoadVCL.

Копирование в буфер обмена

Обратная задача — положить изображение в буфер так, чтобы другие приложения получили альфа-канал. Стандартный VCL кладёт только CF_BITMAP, и получатель оказывается в той же ситуации, что и мы в начале статьи.

Модуль решает это симметрично:

TrySaveGraphicToClipboard кладёт в буфер сразу два формата:

  • PNG: для приложений, которые его понимают (браузеры, Photoshop, GIMP, Paint.NET);
  • CF_DIBV5: для приложений, читающих DIB с альфой, но не читающих PNG.

И, если не получилось с обоими, кладёт изображение через VCL: Clipboard.Assign(Graphic).

Остальное Windows синтезирует сам: CF_DIB и CF_BITMAP появятся в буфере автоматически. Так что приложения без поддержки альфы тоже получат картинку — просто без прозрачности.

Входной TGraphic может быть любым — TBitmap, TPngImage, даже TMetafile. Модуль сам конвертирует его в 32-битный битмап с альфой перед записью. Если у исходного изображения нет альфа-канала, все пиксели получат альфу 255 (полная непрозрачность).

Как и при загрузке, отдельные функции доступны напрямую: TrySavePNG, TrySaveDIBV5, TrySaveVCL.

Листинг модуля IP76.Imaging.Clipbrd

Модуль, как мне кажется, очень подробно прокомментирован. Если всё равно останутся вопросы, обращайтесь, либо в комментариях к статье, либо в телеге, обязательно отвечу.

IP76.Imaging.Clipbrd

[свернуть]

Заключение

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

Решение — работать с буфером через PNG и CF_DIBV5 напрямую. Модуль IP76.Imaging.Clipbrd делает это за вас: две функции, TryLoadGraphicFromClipboard и TrySaveGraphicToClipboard, закрывают оба направления.


Скачать

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

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

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


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

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