HotPDF рендерит группу прозрачности PDF, нарисованную с blend-режимом не-Normal, позволяя группе вычислить собственное смешивание: Cb(1 − α) + α·B(Cb, Cs) — с альфой группы, которую он отслеживает или восстанавливает из чёрного и белого проходов. Начиная с v2.770.0 свойство THotPDF.RenderColorimetricLuminosityMasks вдобавок выбирает, читают ли luminosity soft mask цвет как взвешенный RGB, как обычные вьюеры, или как CIE Y, как описывает ISO 32000-1
Оба изменения выросли из одного и того же типа файла: страница, экспортированная из дизайн-инструмента, где фотография товара лежит на мягкой отбрасываемой тени. Тень — это Form XObject, нарисованный как группа прозрачности с /BM /Multiply, а её затухание — luminosity soft mask. В других вьюерах тень — светло-нейтральный серый. В старых сборках HotPDF она выходила почти чёрной и подкрашенной в цвет фона. Два отдельных бага лежали друг на друге, и их стоит разобрать, потому что любой рендерер, выросший из дизайна «сначала непрозрачные объекты», поймает оба
Почему отбрасываемая тень Multiply рендерится слишком тёмной?
Тень Multiply выходит слишком тёмной, когда рендерер смешивает композитный результат группы вместо её собственного цвета и альфы. Полупрозрачные пиксели этого результата уже содержат фон, поэтому смешивание их с фоном умножает фон на самого себя. Normal-смешивание с непрозрачностью линейно и переживает эту ошибку незамеченным; Multiply, Screen, Darken и все прочие нелинейные режимы — нет
Конвейер отрисовки HotPDF изначально обрабатывал blend-режимы по нарисованному объекту простой и быстрой моделью. Перед рисованием объекта рендерер снимает слепок затронутых пикселей; после рисования сравнивает битмапы «до» и «после» и трактует цвет «после» как непрозрачный источник Cs, вычисляя B(Cb, Cs) и компонуя его с текущей непрозрачностью. Для залитого пути, глифа или непрозрачного изображения это ровно правильно: источник действительно непрозрачен внутри своей формы, и форма — это то место, где пиксели изменились
Группа прозрачности ломает это допущение. ISO 32000-1 §11.4 определяет группу как то, что сначала компонуется в собственный результат, со своим цветом и своей альфой, и лишь затем смешивается с фоном как единый объект. Там, где тень непрозрачна на 30 процентов, битмап «после» держит примерно 0.3 цвета тени плюс 0.7 страницы под ней. Скормите это Multiply как непрозрачный источник, и формула превратится в «фон, умноженный на значение, уже содержащее фон»: тень темнеет и подхватывает оттенок фона. Ничего не падает, никакая диагностика не срабатывает, страница просто выглядит неправильно — а это худший способ отказа для рендерера
Как HotPDF позволяет группе прозрачности смешиваться самостоятельно
С v2.769.3 HotPDF выносит смешивание группы, нарисованной с blend-режимом не-Normal, из общего пообъектного пути в сам рендерер группы. Группа вычисляет Cb(1 − α) + α·B(Cb, Cs) прямо из собственного цвета, собственной альфы и слепка фона, снятого для оператора Do, затем помечает результат как уже смешанный, так что охватывающий Do лишь компонует его с текущей непрозрачностью, не смешивая второй раз. Обычные объекты сохраняют быструю модель «до-после», а группы под Normal-смешиванием не затронуты, потому что линейный случай никогда не был ошибочным
Трудная часть — альфа. Изолированная группа (/I true) стартует с полностью прозрачного фона, поэтому рендерер уже ведёт для неё отдельную альфа-плоскость и может читать α напрямую. Неизолированная группа компонуется на то, что уже есть на странице, и собственной альфа-плоскости не имеет. HotPDF восстанавливает её, рисуя группу дважды — один раз на чёрном, один раз на белом:
- На чёрном каждый пиксель выходит как
α·Cs - На белом тот же пиксель выходит как
α·Cs + (1 − α) - Вычитание даёт
α = 1 − (white − black), а затемCs = black / α
Это работает, потому что каждая Normal-компоновка, которую выполняет рендерер, включая GDI AlphaBlend для изображений с soft mask и сглаженными краями, линейна по фону. У трюка есть предусловие: содержимое группы само должно смешиваться нормально. Если собственные ресурсы ExtGState группы ставят не-Normal /BM, два прохода перестают отличаться на чистый линейный член, и HotPDF не разделяет группу, откатываясь к прежнему поведению. Проверка охватывает только собственные ресурсы группы, а не вложенные формы. Цена тоже реальна: такие страницы рендерятся несколько медленнее, потому что группа рисуется дважды. Soft mask, построенные внутри группы, хранятся, пока оба прохода не закончатся, так что второй проход попадает в кэш масок, а не строит их заново
// Иллюстрация восстановления альфы по каналам (значения 0..1).
// Black = группа на чёрном фоне, White = на белом фоне.
procedure RecoverGroupPixel(Black, White: Double;
out Alpha, Source: Double);
begin
Alpha := 1.0 - (White - Black);
if Alpha < 0.0 then Alpha := 0.0;
if Alpha > 1.0 then Alpha := 1.0;
if Alpha > 0.0 then
Source := Black / Alpha // делим на альфу (un-premultiply)
else
Source := 0.0; // полностью прозрачно: цвет не важен
end;
// Multiply, затем компоновка группы Cb(1 - a) + a * B(Cb, Cs)
function CompositeMultiply(Backdrop, Source, Alpha: Double): Double;
begin
Result := Backdrop * (1.0 - Alpha) + Alpha * (Backdrop * Source);
end;
Код выше — иллюстрация математики, а не API HotPDF; рендерер делает то же самое по каналам и пикселям внутри THotPDF.RenderLoadedPageToBitmap. На практике альфа, восстановленная из двух 8-битных проходов, теряет точность на пути к нулю, поэтому совсем бледные края несут шум квантования — вот почему деление осмысленно только там, где альфа явно выше нуля
Blend-режим сбрасывается в Normal внутри группы
Blend-режим, как постоянная альфа и soft mask, относится к группе прозрачности целиком, а не к каждому объекту внутри неё, поэтому содержимое группы обязано стартовать с Normal-смешивания (ISO 32000-1 §11.6.6). HotPDF раньше сбрасывал непрозрачность и soft mask при входе в группу, но blend-режим не трогал. Результат внутри изолированной группы был эффектным: контент умножал сам себя на пустой начальный фон группы, который чёрный, и вся группа рисовалась чёрной
Тот же сброс действует при построении soft mask. Если один ExtGState ставит и /BM /Multiply, и /SMask, группа маски обязана рисоваться с Normal-смешиванием; иначе белая группа маски чернеет и прячет ровно то, что маска должна была показать. Оба сброса — часть v2.769.3, и оба невидимы на простых документах, потому и дожили до наших дней
Какое значение должна принимать luminosity soft mask для средне-серого?
По ISO 32000-1 luminosity soft mask, чья группа использует CIE-based пространство смешивания, должна принимать CIE 1931 Y, а Y для sRGB средне-серого 0.5 — всего около 0.21, потому что Y — линейный свет. Обычные PDF-вьюеры вместо этого сводят цвет группы к RGB и взвешивают закодированные значения примерно 0.30 / 0.59 / 0.11, что даёт около 0.5 для того же серого. С v2.770.0 HotPDF по умолчанию использует вьюер-совместимое взвешивание и предлагает THotPDF.RenderColorimetricLuminosityMasks для прочтения по спецификации
Разрыв не академический. Затухание в экспорте из дизайн-инструмента — обычно градиент в ICCBased или калиброванной группе, а полутона — ровно то место, где линейный Y и гамма-кодированный серый расходятся сильнее всего. С CIE Y маска средне-серого пропускает лишь около пятой части тени вместо половины, так что отбрасываемые тени и виньетки выходят заметно светлее, чем в любом другом вьюере, куда клиент откроет файл. Быть правым по букве стандарта и неправым по глазам всех остальных — плохая сделка для дефолта, поэтому дефолт следует за вьюерами, а строгий режим — на расстоянии одного свойства
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('catalog-page.pdf') > 0 then
begin
// По умолчанию (False): luminosity-маски взвешивают сведённый цвет
// как DeviceRGB, совпадая с тем, что показывают обычные вьюеры.
// True: брать CIE Y, когда у группы маски CIE-based пространство.
Pdf.RenderColorimetricLuminosityMasks := False;
Bmp := Pdf.RenderLoadedPageToBitmap(0, 150);
if Bmp <> nil then
try
Bmp.SaveToFile('catalog-page-viewer.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Свойство имеет значение, только когда пространство смешивания группы маски — CIE-based: CalGray, CalRGB, Lab или ICCBased. Маски, чья группа использует DeviceGray, DeviceRGB или DeviceCMYK, при любой настройке рендерятся ровно как раньше, поскольку конверсия device-пространства в серый одинакова в обоих режимах
Подводные камни: /BC, /CS и кэш отрисовки
Luminosity soft mask ровно настолько же согласован, насколько согласован его фон: цвет /BC, заливающий область маски вне группы, обязан конвертироваться по тому же правилу, что и краска внутри группы. HotPDF поменял и то и другое вместе в v2.770.0. Конверсия краски взвешенным RGB, а фона — CIE Y даёт видимый шов там, где кончается bounding box группы, — баг, который легко внести, когда фон проходит через другой хелпер, нежели контент
Атрибуты группы к тому же должны быть валидны прежде, чем что-либо из этого случится. HotPDF читает пространство смешивания из записи /CS группы и требует, чтобы /BC нёс ровно одно число на компоненту этого пространства: три для RGB-based пространства, четыре для CMYK, одно для серого. Отсутствующий /CS или /BC неверной арности валит маску до выполнения группы, и краска рисуется без маски, а не с маской на угаданном фоне. Separation, DeviceN и NChannel — не легальные пространства смешивания, поэтому плашечная краска внутри группы маски проходит через tint transform в альтернативное пространство, прежде чем будет вычислена светимость
Наконец, настройка меняет пиксели, а значит, входит в ключ кэша. Битмапы, отрисованные с включённым и выключенным RenderColorimetricLuminosityMasks, кэшируются отдельно и в memory-, и в дисковом кэше отрисовки, и переключение свойства между вызовами THotPDF.RenderLoadedPageToBitmapCached никогда не вернёт битмап из другого режима. Одно эксплуатационное замечание: v2.769.3 внесла регрессию только под Win64, где страницы с группой прозрачности, не задающей собственной soft mask, не рендерились; исправлено в v2.770.1, так что 64-битным приложениям стоит использовать тот релиз или новее
// Цветопроба бок о бок: рендерим ту же страницу в обоих режимах.
// Каждый режим кэшируется под своим ключом, поэтому повторные вызовы дёшевы.
Pdf.RenderColorimetricLuminosityMasks := False;
ViewerBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
Pdf.RenderColorimetricLuminosityMasks := True;
StrictBmp := Pdf.RenderLoadedPageToBitmapCached(PageIndex, 96);
// ... показать обе, затем освободить: кэш-вызовы отдают копии во владение вызывающего
ViewerBmp.Free;
StrictBmp.Free;
О более широком API отрисовки — выборе DPI, кэше страниц и том, что рендерер всё ещё аппроксимирует, — смотрите рендеринг страницы PDF в Delphi TBitmap с HotPDF. Если ваша задача в обратную сторону — убрать прозрачность из файла ради печати или архивирования, а не нарисовать её, — об этом soft mask и уплощение прозрачности в PDFlibPas
FAQ
Рендерит ли HotPDF blend-режимы PDF внутри групп прозрачности правильно?
Да, с v2.769.3. Группа, нарисованная с Multiply или другим не-Normal blend-режимом, смешивает собственный цвет и альфу с фоном, а её контент стартует с Normal-смешивания, как того требует ISO 32000-1 §11.6.6
Почему HotPDF рендерит неизолированные группы дважды?
У неизолированной группы нет собственной альфа-плоскости. HotPDF рисует её на чёрном и на белом и восстанавливает альфу как 1 − (white − black), что стоит части скорости, но делает тени Multiply правильными
Ставить ли RenderColorimetricLuminosityMasks в True?
Оставьте False, чтобы совпадать с тем, что показывают обычные PDF-вьюеры. Ставьте True, только когда нужно прочтение CIE Y из ISO 32000-1 для CIE-based групп масок — например, при сравнении на соответствие
Группы прозрачности с blend-режимами, luminosity soft mask и рендерер страниц, который их рисует, — часть HotPDF Delphi Component, нативной VCL-библиотеки для Delphi и C++Builder, создающей, редактирующей и рендерящей PDF-документы без внешних DLL