PDFium Component создаёт осевые и радиальные градиенты PDF в Delphi через TPdf.SaveAsShadingCreated. Вы описываете каждый градиент записью TPdfShadingCreate (тип, цветовое пространство, координаты, цвета или стопы, целевая страница), а метод пишет проверенную копию, в которой каждая заливка — настоящий словарь shading из ISO 32000-1, зарегистрированный в словаре /Resources /Shading страницы и рисуемый оператором sh, так что PDFium рендерит его нативно
Потребность всплывает в генераторах отчётов и сертификатов. Дизайнер отдаёт макет с мягкой сине-белой полосой шапки или радиальным свечением за печатью, а объектно-страничный API, покрывающий текст, пути и изображения, для этого ничего не имеет. Можно сымитировать градиент сотней тонких прямоугольников чуть разных цветов, но результат полосит при увеличении, раздувает content stream и плохо печатается. В PDF гладкие shading есть с версии 1.3; работа в том, чтобы написать их так, чтобы рендерер их действительно нарисовал
Почему PDFium Component пишет градиенты отдельным сохранением?
TPdf.SaveAsShadingCreated — операция сохранения, а не вызов страничных объектов, потому что у публичного ABI PDFium нет функции, создающей словарь shading или выпускающей оператор sh. Слой объектов страницы, используемый при создании PDF с нуля с PDFium Component, строит пути, текст и изображения, и на этом останавливается. Shading живут уровнем ниже — в графе объектов и синтаксисе content stream
Поэтому метод работает стадиями. Сперва PDFium нативно сохраняет открытый документ, что бережёт каждую правку, сделанную через обычный API. Затем PDFium Component переписывает тот поставленный на подготовку вывод: выделяет по одному косвенному объекту на каждый shading, добавляет ресурсную запись каждой целевой странице, дописывает новый content stream в /Contents страницы и пишет свежую таблицу перекрёстных ссылок. Прежде чем хоть что-то попадёт в ваш поток или файл, результат перезагружается со строгой проверкой перекрёстных ссылок, и метод возвращает False с сообщением в отчёте, если та перезагрузка провалилась. Файловая перегрузка сперва пишет во временный файл, так что неудачное сохранение не затирает существующую цель
Построение двухцветного осевого градиента
Осевой shading (ISO 32000-1 §8.7.4.5.3, Type 2) переливает цвета вдоль линии от x0 y0 до x1 y1, поэтому Coords принимает четыре значения. Радиальный shading (§8.7.4.5.4, Type 3) переливает между двумя окружностями и принимает шесть: x0 y0 r0 x1 y1 r1. С обычными StartColor и EndColor PDFium Component выпускает экспоненциальную функцию Type 2 (§7.10.3) с /N 1, что есть прямая линейная интерполяция, и выставляет /Extend [true true], чтобы конечные цвета продолжались за оба конца оси
uses
System.SysUtils, PDFium, FPdfCompress;
procedure AddHeaderBand(const InFile, OutFile: string);
var
Pdf: TPdf;
Fills: TPdfShadingCreateShadings;
Report: TPdfShadingCreateReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := InFile;
Pdf.Active := True;
SetLength(Fills, 1);
Fills[0] := Default(TPdfShadingCreate);
Fills[0].ShadingType := pstAxial;
Fills[0].ColorSpace := pscDeviceRGB;
Fills[0].Coords := [0, 792, 0, 640]; // x0 y0 x1 y1, сверху вниз
Fills[0].StartColor := [0.12, 0.35, 0.75]; // тёмно-синий у верхнего края
Fills[0].EndColor := [1, 1, 1]; // белый ниже
Fills[0].PageNumber := 1; // нумерация с единицы
Fills[0].ResourceName := 'HeaderBand'; // опционально, по умолчанию Sh1, Sh2...
if not Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
raise Exception.Create('Shading creation failed: ' + Report.ErrorMessage);
finally
Pdf.Free;
end;
end;
Число компонентов каждого цвета обязано совпадать с цветовым пространством: один для pscDeviceGray и pscCalGray, три для pscDeviceRGB, pscCalRGB и pscLab, четыре для pscDeviceCMYK. Для pscICCBased число приходит из байтов профиля, переданного в IccProfile и обязанного выставлять 1, 3 или 4 компонента; профиль встраивается собственным ICC-потоком с соответствующим /N. Калиброванные пространства принимают опциональные CalWhitePoint (три значения, положительный Y), CalGamma (одно значение для CalGray, три для CalRGB) и, для Lab, шестизначный LabRange. Каждая координата и компонент проверяются на NaN и бесконечность прежде, чем будет записан хоть байт
Учтите, что координаты заданы в пользовательском пространстве по умолчанию, с началом в левом нижнем углу страницы. И помните, что ось определяет лишь, где случается перелив: крашеная область — всё, что позволяет клип, а это важнее, чем звучит, как покажут следующие разделы
Многостоповые градиенты компилируются в сшивающую функцию Type 3
PDFium Component превращает массив Stops в функцию, какую ждёт PDF-ридер: два стопа дают одну функцию Type 2, а от 3 до 256 стопов — сшивающую функцию Type 3 (ISO 32000-1 §7.10.4) из одного линейного сегмента Type 2 на каждую смежную пару. Каждая запись TPdfShadingCreateStop несёт Position и Color. Непустой Stops целиком заменяет StartColor и EndColor
Сшивающей функции нужны три массива, которые вручную легко испортить тонко. /Functions держит сегменты, /Bounds — только внутренние позиции стопов (k сегментов требуют k-1 границ), а /Encode отображает каждый поддомен обратно в [0 1], чтобы каждый сегмент видел весь свой входной диапазон. Компонент выводит все три из ваших стопов, поэтому строг к входу: позиции обязаны быть конечными, строго возрастающими, начинаться с 0 и заканчиваться 1 (в пределах 1e-6). Список стопов, начинающийся с 0.1, оставил бы часть домена без определённой функции, и метод его отвергает, а не угадывает
function Stop(Position: Double; const Rgb: array of Double): TPdfShadingCreateStop;
var
I: Integer;
begin
Result.Position := Position;
SetLength(Result.Color, Length(Rgb));
for I := 0 to High(Rgb) do
Result.Color[I] := Rgb[I];
end;
procedure BuildSealGlow(var Fill: TPdfShadingCreate);
begin
Fill := Default(TPdfShadingCreate);
Fill.ShadingType := pstRadial;
Fill.ColorSpace := pscDeviceRGB;
// x0 y0 r0 x1 y1 r1: точка в центре печати, растущая до круга 90 pt
Fill.Coords := [450, 160, 0, 450, 160, 90];
Fill.Stops := [Stop(0.0, [1.00, 0.95, 0.70]),
Stop(0.4, [0.95, 0.75, 0.30]),
Stop(1.0, [1.00, 1.00, 1.00])];
Fill.PageNumber := 1;
end;
Для трёхстопового градиента это даёт /Functions с двумя сегментами Type 2, /Bounds [0.4] и /Encode [0 1 0 1]. Границы сегментов — жёсткие цветовые стыки, поэтому список стопов не сглаживает через них; если стык кажется слишком резким, добавляйте стопы, а не ждите от ридера интерполяции кривизны
Почему PDFium молча не рисует ничего?
PDFium рендерит свежесозданный shading, только когда верны две детали: /ShadingType обязан быть целым числом, а вызов sh обязан сидеть внутри клиппирующего пути. Ошибитесь в любой — и файл открывается, структурно валидируется и показывает нетронутую страницу. Без ошибки, без предупреждения, без строчки в логе
Грабли первые: /ShadingType — число, а не имя
ISO 32000-1 Table 78 определяет /ShadingType как целое: 2 для осевого, 3 для радиального. Первая версия этого писателя выпускала /ShadingType /Axial, потому что в hex-дампе это читалось лучше. PDFium читает ключ целочисленным геттером, получает для имени 0, классифицирует shading как невалидный, не может его загрузить и пропускает оператор sh без жалоб. Страница рендерилась пустой, и ничто в PDFium не говорило почему. Причина стала видна только после постройки минимального рукописного PDF и открытия его во втором независимом рендерере, который сообщил неизвестный тип shading 0. Урок обобщается на любого писателя PDF: когда спецификация типизирует перечисление целым, пишите целое, каким бы заманчивым ни выглядело самоописывающее имя
Грабли вторые: content stream с одним лишь sh не имеет габаритного бокса
Оператор sh (ISO 32000-1 §8.7.4.2) красит shading по текущей клиппируемой области, так что на бумаге его ограничивает собственная протяжённость страницы. Однако PDFium строит объект shading-страницы, чей прямоугольник берётся либо из активного клип-пути, либо из габаритного бокса, накопленного прежними рисующими операторами того же потока, и рендерер отсекает объекты с пустым прямоугольником. Свежедописанный поток, содержащий ничего, кроме /Sh1 sh, не имеет ни того, ни другого, поэтому градиент выбрасывается до растеризации. TPdf.SaveAsShadingCreated потому читает crop box каждой страницы (с откатом к media box, затем к размеру страницы) и оборачивает каждый вызов явным клипом re W n. На странице US Letter с началом в нуле дописанный поток выглядит так:
q
0 0 612 792 re W n
/HeaderBand sh
Q
Пара q/Q изолирует клип и любую трансформацию от существующего содержимого страницы, так что ничто нарисованное позже на странице их не наследует. Если пишете вызовы shading сами в другом инструменте, скопируйте эту форму: явный клип в том же потоке дёшев и снимает зависимость от того, что было раньше
Как кастомная матрица градиента взаимодействует с клипом?
Матрица градиента в PDFium Component применяется как оператор cm, поставленный после клипа и перед sh, поэтому клип остаётся на всю страницу, а трансформируется только геометрия градиента. Поставьте HasMatrix в True и заполните MatrixA по MatrixF — те же шесть чисел, что у любой матрицы трансформации PDF. Значения обязаны быть конечными, а определитель ненулевым, потому что сингулярная матрица схлопнула бы shading в линию. Компонент также записывает те шесть значений как /Matrix в словарь shading, но для прямого вызова sh систему координат, в которой красит PDFium, задаёт именно cm в content stream
Порядок здесь важен, и это третья ловушка. Операнды re интерпретируются в текущей матрице трансформации. Поставьте cm первым — и страничный клип-прямоугольник масштабируется, поворачивается или скашивается вместе с градиентом, так что матрица, сплющивающая эллипс, сплющивает и видимую область, и части страницы теряют заливку. Сперва клип в нетрансформированном пользовательском пространстве, затем конкатенация матрицы — и получается ожидаемое: трансформированный градиент, покрывающий всю страницу. В руководстве по штампам Form XObject и их шестизначной матрице разобрано, что делает каждый компонент матрицы, если вам нужно собрать свою
procedure MakeEllipticalGlow(var Fill: TPdfShadingCreate);
begin
BuildSealGlow(Fill); // радиальная заливка с центром в 450,160
Fill.Coords := [0, 0, 0, 0, 0, 90]; // окружности в начале координат
Fill.HasMatrix := True;
Fill.MatrixA := 1.6; // растянуть по горизонтали
Fill.MatrixB := 0;
Fill.MatrixC := 0;
Fill.MatrixD := 0.8; // сжать по вертикали
Fill.MatrixE := 450; // затем перенести в центр печати
Fill.MatrixF := 160;
end;
Чтение отчёта и знание пределов
TPdfShadingCreateReport сообщает, что было записано: SourceByteCount и OutputByteCount, CreatedShadingObjectNumbers (по номеру объекта на каждый входной элемент, по порядку), AttachedShadingCount, Succeeded и ErrorMessage. Ошибки валидации называют элемент и поле, например список стопов, не строго возрастающий, так что их стоит логировать дословно
if Pdf.SaveAsShadingCreated(OutFile, Fills, Report) then
Writeln(Format('%d shading(s) attached, %d -> %d bytes',
[Report.AttachedShadingCount, Report.SourceByteCount,
Report.OutputByteCount]))
else
Writeln('Rejected: ' + Report.ErrorMessage);
Профиль нарочно ограничен. Создаются только осевые и радиальные shading; function-based и mesh shading (Types 1 и 4–7) и паттерновые цветовые пространства отвергаются. Один вызов принимает от 1 до 4096 заливок, номера страниц обязаны существовать, а имена ресурсов ограничены 127 символами букв, цифр, подчёркивания, минуса и точки. Зашифрованный вход отвергается на стадии trailer. Подписанному документу нужно AllowSignedDocument := True, и поскольку вывод — полная перезапись, а не инкрементальное обновление, существующие подписи больше не валидируются, так что подписывайте после добавления градиентов. Каждая заливка рисуется в конце содержимого страницы, поверх уже имеющегося; для фонового эффекта создавайте градиент первым и добавляйте текст и изображения после, либо используйте optional content, как описано в статье об управлении слоями optional content PDF в Delphi, когда заливка должна быть переключаемой
FAQ
Поддерживает ли SaveAsShadingCreated многостоповые градиенты?
Да. Передавайте от 2 до 256 записей в Stops с позициями, строго возрастающими от 0 до 1. Два стопа дают функцию Type 2, а три и более — сшивающую функцию Type 3 с выведенными массивами /Bounds и /Encode
Почему мой градиент невидим в PDFium, хотя файл выглядит валидным?
Проверьте, что /ShadingType — целое 2 или 3, а не имя, и что оператор sh выполняется внутри клипа re W n в том же content stream. PDFium молча пропускает невалидные типы shading и отсекает объекты shading с пустым прямоугольником
Какие цветовые пространства может использовать созданный shading?
TPdfShadingCreate принимает DeviceGray, DeviceRGB, DeviceCMYK, CalGray, CalRGB, Lab и ICCBased с профилем на 1, 3 или 4 компонента. Паттерновые и Indexed цветовые пространства не входят в профиль создания
Создание градиентов, optional content и остальные staged-писатели поставляются с PDFium Component for Delphi and C++Builder, который оборачивает PDFium для проектов на Delphi, C++Builder и Lazarus