Нанесение водяного знака или логотипа на каждую страницу документа кажется пятиминутной задачей, пока вы не откроете результат в инспекторе размера файла. Очевидный подход заключается в том, чтобы пройтись по страницам и на каждой из них заново создать одни и те же текстовые или графические объекты. Визуально это работает, но это расточительно в геометрической прогрессии. Диагональный водяной знак «DRAFT», нарисованный непосредственно в стостраничном отчете, — это сотня копий одного и того же пути и текстовых данных, находящихся в потоках содержимого, и сохраненный файл содержит их все
Form XObject — это конструкция, предоставляемая PDF, чтобы избежать именно этого. Она оборачивает часть многоразового содержимого, целую страницу или небольшой шаблон, в один именованный объект, который можно отрисовать множество раз в разных позициях. Содержимое находится в файле один раз. Каждая страница, на которой нужен штамп, содержит короткую инструкцию, которая говорит: «отрисуйте XObject N здесь с таким преобразованием». В результате водяной знак на сто страниц добавляет в файл один объект содержимого, а не сто, и в этом заключается разница между документом, который линейно растет в зависимости от количества страниц, и тем, который не растет. Водяные знаки, штампы с логотипами, шаблоны номеров страниц и печати — это одна и та же проблема, и Form XObject — правильный инструмент для каждой из них
Почему один сохраненный объект лучше сотни перерисовок
Экономия носит структурный, а не косметический характер. Страница PDF отрисовывается путем выполнения потока ее содержимого, последовательности операторов рисования. Когда вы перерисовываете штамп на каждой странице, вы добавляете полную последовательность операторов для этого штампа в поток каждой страницы, и байты дублируются столько раз, сколько у вас страниц. Form XObject перемещает эти операторы в один поток, хранящийся в документе один раз. Ссылка, которую хранит отдельная страница, невелика: она помещает матрицу преобразования в стек, вызывает XObject и восстанавливает состояние. Количество страниц больше не увеличивает стоимость графики
Это наиболее важно, когда штамп тяжелый. Векторную печать с сотнями сегментов пути или растровый логотип дорого хранить. При однократном сохранении и использовании по ссылке тяжелая часть оплачивается один раз, а накладные расходы на каждую страницу составляют несколько байт вызова. Визуальный результат на странице идентичен прямой перерисовке, в чем и заключается суть. Читатель не может заметить разницы, но размер файла вполне может
Захват страницы в XObject
PDFium создает многоразовый объект из существующей страницы. Источником является страница в некотором открытом вами документе, небольшом одностраничном PDF-файле, который не содержит ничего, кроме рисунка водяного знака, или определенная страница более крупного файла. CreateXObjectFromPage захватывает содержимое этой исходной страницы в многоразовый дескриптор, принадлежащий целевому документу, на который вы ставите штамп
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := 'Report.pdf';
Dest.Active := True;
Stamp.FileName := 'Watermark.pdf'; // one page of artwork
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 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. Метод вызывает исключение, если исходный документ не Active, и он возвращает nil, а не вызывает исключение, когда PDFium не может создать объект, поэтому явная проверка выше не является необязательной. Возвращаемый дескриптор — это принадлежащий вам TPdfXObject, и два связанных с ним ограничения времени жизни — это часть всего этого упражнения, которая ловит людей, поэтому для них выделен отдельный раздел ниже
Размещение штампа на странице
Захваченный XObject ничего не делает сам по себе. Чтобы он появился, вы вставляете его копию на текущую страницу документа (ту, которая выбрана свойством PageNumber, начинающимся с 1) с помощью InsertFormObjectFromXObject. Этот вызов возвращает базовый объект страницы, FPDF_PAGEOBJECT, и возвращенный дескриптор используется для позиционирования размещения. Без преобразования штамп приземляется в начало координат исходной страницы, что редко бывает там, где вам нужно
Поскольку InsertFormObjectFromXObject вставляет одну копию за вызов и каждый раз возвращает новый объект страницы, вы можете нарисовать один и тот же XObject несколько раз на одной странице с разными преобразованиями, и сохраненное содержимое по-прежнему будет учитываться в файле один раз. Угловой логотип и тусклый полностраничный водяной знак могут исходить из одного и того же захваченного объекта
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
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);
RawM := M.Handle;
if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
raise Exception.Create('Cannot assign the stamp matrix');
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits to its content stream
// if not Dest.SaveAs(...) then ... when every page is done.
end;
Две детали управления делают это безопасным. Во-первых, после вставки объект страницы принадлежит странице, а не XObject. Освобождение XObject позже не делает недействительными размещения, которые вы уже сделали. Именно это позволяет работать описанному ниже порядку «создать-разместить-освободить». Во-вторых, вставка и позиционирование изменяют только список объектов страницы в памяти; UpdatePage — это то, что сериализует этот список обратно в поток содержимого страницы, поэтому страница, которую вы редактируете без ее вызова, сохраняется так, как если бы штамп никогда не был установлен
Правило времени жизни дескриптора, на котором спотыкаются люди
Дескриптор XObject подчиняется двум ограничениям, и игнорирование любого из них приводит к сбою, который выглядит не связанным с его причиной. Во-первых, исходный документ должен быть активен в момент вызова CreateXObjectFromPage. Захват считывает содержимое исходной страницы из живого исходного документа, поэтому этот документ и его страница должны быть открыты и действительны при создании дескриптора. Во-вторых, и это то, что удивляет людей, дескриптор должен быть освобожден до закрытия исходной страницы, а на практике — до закрытия или освобождения исходного документа, из которого он был получен
Причина в том, что XObject — это ссылка на структуру, которой все еще владеет исходный документ. Это не отсоединенная, автономная копия, которую можно носить с собой после того, как источник исчезнет. Сначала закройте источник, и дескриптор останется указывать на уничтоженное содержимое, поэтому его последующее освобождение или любое другое использование будет оперировать памятью, которая больше не действительна. Симптом является классическим для висячего дескриптора: нарушение прав доступа при завершении работы (access violation) или периодическое повреждение, которое перемещается в зависимости от порядка выделения, со стеком, который указывает на код очистки, а не на строку, которая фактически вызвала проблему. Решение кроется в порядке действий, а не в защитном программировании. Создайте XObject, вставьте его на каждую страницу, где это необходимо, освободите XObject и только затем закройте исходный документ. Деструктор TPdfXObject освобождает базовый дескриптор PDFium за вас, поэтому своевременное освобождение обертки — это полностью ваша ответственность
Матрица и что означают ее шесть чисел
Размещение — это 2D аффинное преобразование, то же самое, которое 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 из модуля FPdfMatrix компонует для вас общие операции и выполняет умножение справа по ходу дела, поэтому Translate, Scale и Rotate объединяются в цепочку в том порядке, в котором вы их вызываете. Диагональный водяной знак — это вращение, за которым следует перемещение для повторного центрирования; угловой логотип — это масштабирование, за которым следует перемещение. Когда матрица готова, скопируйте ее необработанное значение, свойство Handle типа FS_MATRIX, в локальную переменную и передайте его в FPDFPageObj_SetMatrix; импорт объявляет матрицу как параметр var, поэтому свойство нельзя передать ей напрямую, и при сбое ее результат будет равен 0. Низкоуровневая функция FPDFPageObj_Transform, которая принимает шесть значений непосредственно в виде чисел двойной точности (doubles), доступна, когда вы предпочитаете передавать числа, а не создавать обертку
Проставление штампов на каждой странице в правильном порядке
Полный шаблон объединяет детали с тем порядком, которого требует правило времени жизни. Откройте оба документа, захватите штамп один раз, пройдитесь по целевым страницам, поочередно устанавливая PageNumber (начиная с 1), вставляя и позиционируя копию, фиксируя каждую страницу с помощью UpdatePage, затем освободите XObject, затем сохраните с помощью SaveAs, и позвольте исходному документу закрыться последним
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
I: Integer;
begin
Dest := TPdf.Create(nil);
Stamp := TPdf.Create(nil);
try
Dest.FileName := ASource;
Dest.Active := True;
Stamp.FileName := AStamp;
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// 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. PageNumber is 1-based.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := 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
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // commit this page's edits
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + 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, наряду с API для рендеринга, редактирования и работы с документами, которые рассматриваются в других статьях этого блога