Нанесення водяного знака або логотипа на кожну сторінку документа здається простим п'ятихвилинним завданням, поки ви не перевірите розмір отриманого файлу. Очевидний підхід полягає в тому, щоб обійти всі сторінки і на кожній заново побудувати однакові об'єкти тексту чи зображень. Візуально це працює, але призводить до значного та неконтрольованого зростання розміру файлу. Діагональний водяний знак 'DRAFT', намальований безпосередньо на 100-сторінковому звіті — це 100 копій одних і тих самих контурів та тексту в потоках вмісту, і збережений файл міститиме кожну з них.
Form XObject — це конструкція, яку PDF надає саме для того, щоб уникнути такої проблеми. Вона обгортає багаторазовий фрагмент вмісту (цілу сторінку або невеликий шаблон) в один іменований об'єкт, який можна малювати багато разів у різних місцях. Сам вміст зберігається у файлі лише один раз. Кожна сторінка, яка потребує штампа, містить коротку інструкцію на кшталт 'намалювати XObject N тут із таким перетворенням'. У результаті водяний знак на 100 сторінок додає до файлу лише один об'єкт вмісту замість 100, і це є причиною того, що розмір документа не зростає лінійно з кількістю сторінок. Водяні знаки, логотипи, шаблони номерів сторінок та печатки — все це завдання одного типу, і Form XObject є ідеальним інструментом для кожного з них.
Чому один збережений об'єкт кращий за сотню перемальовувань
Економія є структурною, а не косметичною. Сторінка PDF відображається шляхом виконання її потоку вмісту — послідовності графічних операторів. Коли ви перемальовуєте штамп на кожній сторінці, ви додаєте повну послідовність операторів цього штампа до потоку кожної сторінки, і байти дублюються стільки разів, скільки у вас є сторінок. Form XObject переносить ці оператори в один потік, який зберігається в документі лише один раз. Посилання, яке містить окрема сторінка, є дуже коротким: воно додає матрицю перетворення, викликає XObject та відновлює графічний стан. Кількість сторінок більше не множить вартість зберігання графіки.
Це особливо важливо, коли штамп є важким для зберігання. Векторна печатка з сотнями сегментів контурів або растровий логотип потребують багато місця. При одноразовому збереженні та використанні посилань вартість важкої частини оплачується один раз, а накладні витрати на кожну сторінку становлять лише кілька байтів виклику. Візуальний результат на сторінці абсолютно ідентичний прямому перемальовуванню, в чому і полягає суть. Користувач не побачить різниці, але розмір файлу відчує її дуже суттєво.
Захоплення сторінки у Form 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. Подальше звільнення XObject не впливає на вже розміщені елементи. Саме це дозволяє використовувати послідовність 'створення-розміщення-звільнення', описану нижче.
Правило часу життя дескриптора, на якому часто помиляються
Два обмеження визначають роботу з дескриптором 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 для Delphi та C++Builder разом з API рендерингу, редагування та керування документами, що висвітлені в інших публікаціях цього блогу.