Поставянето на воден знак или лого на всяка страница от документ изглежда като петминутна работа, докато не отворите резултата в инспектор за размер на файла. Очевидният подход е да обходите страниците и на всяка една да изградите същите текстови или графични обекти отново. Това работи визуално, но е разточително по начин, който се усложнява. Диагонален воден знак "DRAFT" (ЧЕРНОВА), нарисуван директно върху отчет от сто страници, представлява сто копия на едни и същи данни за път и текст, седящи в потоците от съдържание, и записаният файл носи всяко едно от тях.
Form XObject е конструкцията, която PDF предоставя, за да избегне точно това. Тя обвива част от съдържание за многократна употреба, цяла страница или малък шаблон, в един наименован обект, който може да бъде рисуван многократно на много позиции. Съдържанието живее във файла веднъж. Всяка страница, която иска печат, съдържа кратка инструкция, която казва "нарисувай XObject N тук, с тази трансформация". Воден знак на сто страници тогава добавя един обект със съдържание към файла вместо сто и това е разликата между документ, който расте линейно с броя на страниците си, и такъв, който не расте. Водните знаци, логата, шаблоните за номера на страници и печатите са една и съща форма на проблем и Form XObject е правилният инструмент за всеки един от тях.
Защо един съхранен обект побеждава сто повторни рисувания
Спестяването е структурно, а не козметично. PDF страницата се рендерира чрез изпълнение на нейния поток от съдържание - последователност от оператори за рисуване. Когато рисувате повторно печат на страница, вие добавяте пълната последователност от оператори за този печат към потока на всяка страница и байтовете се дублират толкова пъти, колкото страници имате. Form XObject премества тези оператори в един поток, съхранен веднъж в документа. Препратката, която отделната страница поддържа, е малка: тя избутва матрица за трансформация, извиква XObject и възстановява състоянието. Броят на страниците вече не умножава цената на произведението на изкуството.
Това е най-важно, когато печатът е тежък. Векторен печат със стотици сегменти на пътища или растерно лого е скъп за съхранение. Съхранен веднъж и рефериран, тежката част се плаща веднъж, а разходите за всяка страница са няколко байта за извикване. Визуалният резултат на страницата е идентичен с директното повторно рисуване, което е и целта. Четецът не може да види разликата; размерът на файла обаче много добре я вижда.
Заснемане на страница в XObject
PDFium изгражда обекта за многократна употреба от съществуваща страница. Източникът е страница в някакъв документ, който сте отворили - малък PDF от една страница, който съдържа само логото ви, или конкретна страница от по-голям файл. CreateXObjectFromPage заснема съдържанието на този източник в дескриптор (handle) за многократна употреба, който е собственост на целевия документ, който подпечатвате в момента.
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 вместо вас, така че освобождаването на обвивката в точното време е цялата ви отговорност.
Матрицата и какво означават нейните шест числа
Разполагането е 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 композира общите операции вместо вас и извършва последващо умножение при движение, така че 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);
M.Translate(150, 100);
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, заедно с API за рендериране, редактиране и документи, разгледани на други места в този блог.