Нанесение водяного знака или логотипа на каждую страницу документа кажется простой пятиминутной задачей, пока вы не проверите размер итогового файла. Очевидный подход заключается в циклическом обходе страниц и повторном создании одних и тех же текстовых или графических объектов. Визуально это работает, но приводит к многократному дублированию данных. Диагональный водяной знак «ЧЕРНОВИК», нарисованный непосредственно на каждой странице 100-страничного отчета, создаст сто копий одинаковых данных контуров и текста в потоках содержимого страниц, и сохраненный файл будет содержать каждую из них.
Объект Form XObject представляет собой структуру PDF, созданную специально для предотвращения такого дублирования. Он упаковывает повторяющееся содержимое (страницу целиком или небольшой шаблон) в один именованный объект, который можно отрисовывать многократно в любых координатах. Сами данные сохраняются в файле единожды. Каждая страница, использующая штамп, содержит лишь короткую команду вида «нарисовать XObject N в этой точке с этой матрицей преобразования». В результате 100-страничный водяной знак добавляет в файл один объект содержимого вместо ста, что предотвращает линейный рост размера документа при увеличении числа страниц. Водяные знаки, логотипы, шаблоны номеров страниц и печати относятся к одному классу задач, для решения которых используется Form XObject.
Почему однократное сохранение эффективнее многократного рисования
Оптимизация носит структурный, а не косметический характер. Страница PDF визуализируется путем выполнения ее потока содержимого, представляющего собой последовательность операторов рисования. При повторном рисовании штампа на каждой странице вы добавляете полную последовательность операторов штампа в поток каждой страницы, дублируя данные. Объект Form XObject переносит эти операторы в единый поток, сохраняемый в документе один раз. Ссылка на странице получается компактной: она сохраняет матрицу преобразования, вызывает XObject и восстанавливает исходное состояние. Количество страниц больше не умножает объем графических данных.
Это наиболее критично при использовании сложных штампов. Векторная печать с сотнями сегментов контуров или растровый логотип занимают много места. При однократном сохранении и последующих вызовах по ссылкам «тяжелые» данные записываются один раз, а затраты на каждую страницу ограничиваются несколькими байтами вызова ссылки. Визуальный результат на странице полностью совпадает с обычной отрисовкой, в чем и заключается цель: пользователь не заметит изменений, а размер файла уменьшится.
Захват страницы в XObject
PDFium создает повторно используемый объект на основе существующей страницы. Источником может служить страница открытого документа, небольшой одностраничный файл PDF с графикой водяного знака или страница крупного файла. Метод CreateXObjectFromPage захватывает содержимое исходной страницы в повторно используемый дескриптор, принадлежащий целевому документу.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Сигнатура метода: CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. При ошибке метод возвращает значение nil, а не вызывает исключение, поэтому проверка на корректность результата обязательна. Возвращаемый дескриптор представляет собой объект TPdfXObject, а связанные с ним правила времени жизни часто вызывают сложности у разработчиков, поэтому они подробно рассмотрены ниже.
Размещение штампа на странице
Захваченный XObject сам по себе нигде не отображается. Для вывода на страницу необходимо вставить его копию с помощью метода InsertFormObjectFromXObject. Этот вызов возвращает объект страницы FPDF_PAGEOBJECT, дескриптор которого используется для позиционирования. Без матрицы преобразования штамп размещается в начале координат исходной страницы, что редко соответствует желаемому результату.
Поскольку метод InsertFormObjectFromXObject вставляет по одной копии за вызов, возвращая каждый раз новый объект страницы, вы можете отрисовать один и тот же XObject несколько раз на одной странице с разными параметрами масштаба и сдвига, при этом сами графические данные сохраняются в файле один раз. Угловой логотип и полупрозрачный водяной знак во всю страницу могут использовать один захваченный объект.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Правило времени жизни дескрипторов и типичные ошибки
Два ограничения регулируют работу с дескриптором XObject, и нарушение любого из них вызывает ошибки, причины которых сложно диагностировать. Во-первых, исходный документ должен оставаться активным в момент вызова CreateXObjectFromPage. Захват считывает данные из исходного файла, поэтому исходный документ и его страница должны быть открыты и корректны при создании дескриптора. Во-вторых (и на этом разработчики чаще всего совершают ошибки), дескриптор должен быть освобожден до закрытия исходной страницы, а на практике - до закрытия или освобождения исходного документа, из которого он был получен.
Причина кроется в том, что XObject представляет собой ссылку на структуру данных, принадлежащую исходному документу. Он не является независимой копией, которую можно использовать после закрытия источника. Если закрыть источник раньше, дескриптор будет указывать на уничтоженные данные, поэтому попытка освободить его позже приведет к работе с некорректной памятью. Симптомы типичны для висячих указателей: ошибки обращения к памяти (access violation) при завершении программы или случайное повреждение данных в зависимости от порядка выделения памяти. Решение заключается в соблюдении строгого порядка операций, а не в написании защитного кода. Создайте XObject, вставьте его на все нужные страницы, освободите XObject и только после этого закрывайте исходный документ. Деструктор TPdfXObject автоматически освобождает дескриптор PDFium, поэтому своевременное удаление объекта-обертки является вашей задачей.
Матрица преобразования и значение ее шести параметров
Позиционирование представляет собой двухмерное аффинное преобразование, используемое в PDF для размещения любого содержимого (стандарт ISO 32000-1, раздел 8.3.4). Оно задается шестью числами a, b, c, d, e, f, которые в PDFium представлены структурой FS_MATRIX. Они сопоставляют точки из пространства объекта с пространством страницы:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Эти шесть значений можно заполнить вручную, но ручной расчет часто приводит к ошибкам при повороте объектов, так как поворот связывает параметры a, b, c, d между собой. Класс-обертка TPdfMatrix автоматизирует расчет базовых операций, перемножая матрицы в процессе, поэтому вызовы Translate, Scale и Rotate можно объединять в цепочки. Наклонный водяной знак представляет собой поворот с последующим сдвигом для центрирования; угловой логотип реализуется масштабированием со сдвигом. Когда матрица готова, передайте ее значение функции FPDFPageObj_SetMatrix(PageObj, M.Handle), где параметр M.Handle указывает на структуру FS_MATRIX. Низкоуровневая функция FPDFPageObj_Transform, принимающая шесть значений типа double напрямую, используется, если вы предпочитаете передавать числа без создания объектов-оберток.
Нанесение штампов на страницы в правильном порядке
Итоговый алгоритм объединяет все шаги в последовательности, диктуемой временем жизни объектов. Откройте оба документа, захватите штамп один раз, выполните обход целевых страниц с добавлением и позиционированием копий, освободите XObject, сохраните результат и закройте исходный документ в последнюю очередь.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // diagonal watermark
M.Translate(150, 100); // nudge into position
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Структура блоков try гарантирует безопасность выполнения. Внутренний блок finally освобождает XObject до того, как управление перейдет к внешнему блоку finally, удаляющему Stamp. Это гарантирует освобождение дескриптора при живом источнике даже в случае возникновения исключений внутри цикла. Правильный выбор уровня вложенности блоков берет на себя заботу о времени жизни дескрипторов.
Нанесение штампов представляет собой лишь часть возможностей по редактированию страниц. Если ваш штамп является растровым изображением, а не захваченной страницей, ознакомьтесь со статьей конвертирование изображений в документы PDF с помощью PDFium. Если к документу необходимо прикрепить файл, статья работа с вложениями PDF в Delphi описывает этот процесс. Все эти функции входят в комплект PDFium Component для Delphi и C++Builder.