Поставянето на воден знак или лого на всяка страница от документ изглежда като петминутна работа, докато не отворите резултата в инспектор за размер на файла. Очевидният подход е да обходите страниците и на всяка една да изградите отново същите текстови или графични обекти (image objects). Това работи визуално и е разточително по начин, който се натрупва. Диагонален воден знак "ЧЕРНОВА" (DRAFT), нарисуван директно върху отчет от сто страници, е сто копия на едни и същи данни за път и текст, седящи в потоците от съдържание (content streams), и запазеният файл носи всяко едно от тях
Form XObject е конструкцията, която PDF предоставя, за да се избегне точно това. Тя увива парче многократно използваемо съдържание, цяла страница или малък шаблон, в един единствен именуван обект, който може да бъде нарисуван много пъти на много позиции. Съдържанието живее във файла веднъж. Всяка страница, която иска печата, съдържа кратка инструкция, която казва "нарисувай XObject N тук, с тази трансформация". Воден знак от сто страници след това добавя един обект със съдържание към файла, а не сто, и това е разликата между документ, който расте линейно с броя на страниците си, и такъв, който не го прави. Водните знаци, печатите с лога, шаблоните за номера на страници и печатите (seals) са една и съща форма на проблем и Form XObject е правилният инструмент за всеки един от тях
Защо един съхранен обект побеждава сто прерисувания
Спестяването е структурно, а не козметично. PDF страницата се рендира чрез изпълнение на нейния поток от съдържание (content stream), последователност от оператори за рисуване. Когато прерисувате печат на страница, вие добавяте пълната последователност от оператори за този печат към потока на всяка страница и байтовете се дублират толкова пъти, колкото страници имате. Едно Form XObject премества тези оператори в един поток, съхранен веднъж в документа. Референцията, която отделната страница пази, е малка: тя избутва (pushes) матрица за трансформация, извиква XObject-а и възстановява състоянието. Броят на страниците вече не умножава цената на произведението на изкуството (artwork)
Това има най-голямо значение, когато печатът е тежък. Векторен печат със стотици сегменти от пътя или растерно изображение на лого са скъпи за съхранение. Съхранена веднъж и референцирана, тежката част се заплаща еднократно и разходът (overhead) на страница е няколко байта за извикване. Визуалният резултат на страницата е идентичен с директно прерисуване, което е и целта. Читателят не може да направи разликата; размерът на файла със сигурност може
Заснемане на страница в XObject
PDFium изгражда многократно използваемия обект от съществуваща страница. Източникът е страница в някакъв документ, който имате отворен, малък PDF файл от една страница, който не съдържа нищо друго освен вашия воден знак, или конкретна страница от по-голям файл. CreateXObjectFromPage заснема съдържанието на тази страница източник в многократно използваем манипулатор (reusable handle), който принадлежи към целевия документ, този, който подпечатвате
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'; // една страница с произведение на изкуството
Stamp.Active := True;
if not (Dest.Active and Stamp.Active) then
raise Exception.Create('Could not open the input documents');
// Заснемане на страница 0 от документа с печата в многократно използваем манипулатор, който
// е притежаван от Dest. Източникът трябва да е Active; индексът е базиран на нула.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... поставете го, след това го освободете преди затваряне на Stamp (вижте по-долу) ...
Сигнатурата е CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Методът хвърля грешка (raises), ако документът източник не е Active, и връща nil вместо да хвърля грешка, когато PDFium не може да изгради обекта, така че изричната проверка по-горе не е по избор. Манипулаторът, който се връща, е TPdfXObject, който притежавате, и двете ограничения за времето на живот, прикрепени към него, са частта от цялото това упражнение, която хваща хората неподготвени, така че те получават свой собствен раздел по-долу
Поставяне на печата на страница
Един заснет XObject не прави нищо сам по себе си. За да се появи, вмъквате негово копие на текущата страница на документа, избрана от базираното на 1 свойство PageNumber, с InsertFormObjectFromXObject. Това извикване връща базовия (underlying) обект на страницата, FPDF_PAGEOBJECT, и върнатият манипулатор е начинът, по който позиционирате разположението. Без трансформация печатът каца в началото (origin) в собствените координати на страницата източник, което рядко е мястото, където го искате
Тъй като InsertFormObjectFromXObject вмъква по едно копие на извикване и връща пресен обект на страницата всеки път, можете да нарисувате един и същи XObject няколко пъти на една страница с различни трансформации и съхраненото съдържание все още се брои веднъж във файла. Ъглово лого и блед воден знак на цяла страница могат да дойдат от един и същ заснет обект
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
RawM: FS_MATRIX;
begin
// Текущата страница на Dest получава едно копие на XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Позиционирайте го: преместете с 200 единици надясно, 500 нагоре, при 70% мащаб.
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) редакциите на тази страница в нейния поток от съдържание
// ако не Dest.SaveAs(...) тогава ... когато всяка страница е готова.
end;
Два детайла от домакинството (housekeeping details) правят това безопасно. Първо, след като бъде вмъкнат, обектът на страницата принадлежи на страницата, а не на XObject. Освобождаването на XObject по-късно не обезсилва (invalidate) разположенията, които вече сте направили. Това е, което позволява на подредбата създай-постави-освободи (create-place-free), описана по-долу, да работи. Второ, вмъкването и позиционирането само променят списъка с обекти на страницата в паметта; UpdatePage е това, което сериализира (serialises) този списък обратно в потока от съдържание на страницата, така че страница, която редактирате, без да го извикате, се запазва така, сякаш печатът никога не е бил поставен
Правилото за времето на живот на манипулатора, което хапе хората
Две ограничения управляват манипулатора на XObject и пренебрегването на което и да е от тях води до повреда, която изглежда несвързана с нейната причина. Първо, документът източник трябва да е активен в момента, в който извикате CreateXObjectFromPage. Заснемането прочита съдържанието на страницата източник от живия документ източник, така че този документ и неговата страница трябва да бъдат отворени и валидни, когато манипулаторът се изгражда. Второ, и това е онова, което изненадва хората, манипулаторът трябва да бъде освободен, преди страницата източник да бъде затворена, и на практика преди да затворите или освободите документа източник, от който идва
Причината е, че XObject е референция към структура, която документът източник все още притежава. Той не е отделено (detached), самостоятелно копие, което можете да носите наоколо, след като източникът е изчезнал. Затворете първо източника и манипулаторът остава да сочи към съдържание, което е било разрушено (torn down), така че освобождаването му по-късно или каквото и да е друго използване на него, оперира върху памет, която вече не е валидна. Симптомът е класическият за висящ манипулатор (dangling handle): нарушение на достъпа (access violation) при изключване (shutdown) или периодична корупция (intermittent corruption), която се движи наоколо в зависимост от реда на алокация, със стек, който сочи към код за почистване (cleanup code), а не към реда, който действително е причинил проблема. Поправката е в подреждането, а не в защитното кодиране. Изградете 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 : хоризонтален и вертикален мащаб
// b, c : членовете за срязване (shear) / завъртане
// e, f : транслация (къде началото каца на страницата)
Можете да попълните тези шест стойности на ръка, но композирането им на ръка е мястото, където завъртането се обърква, защото завъртането смесва и четирите от a, b, c, d заедно. Обвивката TPdfMatrix от unit-а FPdfMatrix композира общите операции вместо вас и пост-умножава в движение, така че Translate, Scale и Rotate се свързват (chain) в реда, в който ги извиквате. Диагонален воден знак е завъртане, последвано от транслация за скорошно центриране (recentre it); ъглово лого е мащаб, последван от транслация. Когато матрицата е готова, копирайте нейната сурова стойност, свойството Handle от тип FS_MATRIX, в локална променлива и подайте това на FPDFPageObj_SetMatrix; импортът декларира матрицата като var параметър, така че свойство не може да ѝ бъде подадено директно и нейният резултат е 0 при неуспех. По-ниско ниво FPDFPageObj_Transform, което приема шестте стойности директно като doubles, е достъпно, когато предпочитате да предава числа, вместо да изграждате обвивка
Поставяне на печат на всяка страница, в правилния ред
Пълният модел сглобява парчетата с подредбата, която изисква правилото за времето на живот. Отворете и двата документа, заснемете печата веднъж, обходете целевите страници, като зададете базирания на 1 PageNumber на свой ред и вмъкнете плюс позиционирате копие, ангажирате всяка страница с 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. Заснемете произведението веднъж. Stamp е Active тук.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Поставете копие на всяка страница на Dest. PageNumber е базиран на 1.
for I := 1 to Dest.PageCount do
begin
Dest.PageNumber := I; // направете страница I текуща
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45); // диагонален воден знак
M.Translate(150, 100); // побутване (nudge) в позиция
RawM := M.Handle;
FPDFPageObj_SetMatrix(PageObj, RawM);
finally
M.Free;
end;
Dest.UpdatePage; // ангажирайте редакциите на тази страница
end;
finally
XObject.Free; // 3. освободете ПРЕДИ Stamp да се затвори
end;
// 4. Запишете резултата, докато Dest все още е отворен.
if not Dest.SaveAs(AOutput) then
raise Exception.Create('Could not save ' + AOutput);
finally
Stamp.Free; // източникът се затваря последен
Dest.Free;
end;
end;
Формата на блоковете try върши истинската работа. Вътрешният finally освобождава XObject, преди контролът изобщо да може да достигне до външния finally, който освобождава Stamp, така че манипулаторът винаги се освобождава, докато източникът му е все още жив, дори ако възникне изключение (exception fires) по средата на цикъла. Направете това влагане (nesting) правилно и правилото за времето на живот се грижи за себе си
Поставянето на печати е един ъгъл от по-голям набор от инструменти за изграждане и редактиране на съдържание на страници. Ако вашият печат сам по себе си е изображение, а не заснета страница, конвертиране на изображения към PDF документи с PDFium обхваща получаването на това растерно изображение в документ на първо място. А когато нещото, което искате да носите заедно с видимия печат, е файл, а не мастило на страницата, работа с PDF прикачени файлове в Delphi показва страната на вградения файл (embedded-file). Всичко това се доставя с PDFium Component за Delphi и C++Builder, заедно с API за рендиране, редактиране и документи, обхванати другаде в този блог