Otisknout vodoznak nebo logo na každou stránku dokumentu vypadá jako práce na pět minut, dokud neotevřete výsledek v programu pro kontrolu velikosti souboru. Zřejmým přístupem je projít stránky a na každé z nich znovu sestavit stejné textové nebo obrázkové objekty. To sice funguje vizuálně, ale je to neefektivní způsobem, který se sčítá. Diagonální vodoznak "DRAFT" vykreslený přímo na stostránkovou zprávu představuje sto kopií stejných dat cest a textu v proudu obsahu a uložený soubor nese každou z nich.
Form XObject je konstrukce, kterou formát PDF poskytuje právě k tomu, aby se tomuto předešlo. Zabalí část opakovaně použitelného obsahu – celou stránku nebo malou šablonu – do jediného pojmenovaného objektu, který lze vykreslit mnohokrát na různých pozicích. Obsah žije v souboru pouze jednou. Každá stránka, která vyžaduje razítko, obsahuje krátkou instrukci říkající "vykresli XObject N zde s touto transformací". Stostránkový vodoznak pak do souboru přidá jeden objekt obsahu namísto sta, což představuje rozdíl mezi dokumentem, jehož velikost roste lineárně s počtem stránek, a dokumentem, kde tomu tak není. Vodoznaky, razítka s logem, šablony s čísly stránek a pečetě jsou stejným typem problému a Form XObject je pro každý z nich tím správným nástrojem.
Proč jeden uložený objekt poráží sto překreslení
Úspora je strukturální, nikoli kosmetická. Stránka PDF se vykresluje spouštěním svého proudu obsahu, což je sekvence kreslicích operátorů. Když překreslujete razítko na každé stránce, připojujete celou sekvenci operátorů pro toto razítko do proudu každé stránky a bajty se duplikují tolikrát, kolik máte stránek. Form XObject přesouvá tyto operátory do jednoho proudu uloženého v dokumentu pouze jednou. Odkaz, který si jednotlivá stránka uchovává, je malý: vloží transformační matici, vyvolá XObject a obnoví stav. Počet stránek již nenásobí náklady na grafiku.
To je nejdůležitější, když je razítko datově náročné. Vektorová pečeť se stovkami segmentů cest nebo bitmapa loga jsou náročné na uložení. Uloží-li se jednou a odkazuje se na ně, těžká část se zaplatí pouze jednou a režie na stránku představuje několik bajtů vyvolání. Vizuální výsledek na stránce je identický s přímým překreslením, což je cílem. Čtenář nepozná rozdíl; velikost souboru jej však pozná velmi výrazně.
Zachycení stránky do objektu XObject
PDFium sestavuje opakovaně použitelný objekt z existující stránky. Zdrojem je stránka v otevřeném dokumentu, malé jednostránkové PDF obsahující pouze grafiku vodoznaku nebo konkrétní stránka většího souboru. CreateXObjectFromPage zachytí obsah této zdrojové stránky do opakovaně použitelného handle, který patří cílovému dokumentu, jejž razítkujete.
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) ...
Umístění razítka na stránku
Zachycený XObject sám o sobě nic nedělá. Aby se zobrazil, vložíte jeho kopii na aktuální stránku dokumentu pomocí InsertFormObjectFromXObject. Toto volání vrátí podkladový objekt stránky, FPDF_PAGEOBJECT, a vrácený handle slouží k jeho umístění. Bez transformace přistane razítko v počátku ve vlastních souřadnicích zdrojové stránky, což je málokdy tam, kam jej chcete umístit.
Protože InsertFormObjectFromXObject vkládá jednu kopii na volání a pokaždé vrací nový objekt stránky, můžete stejný XObject vykreslit na jedné stránce několikrát při různých transformacích a uložený obsah se v souboru stále počítá pouze jednou. Rohové logo a jemný celostránkový vodoznak mohou pocházet ze stejného zachyceného objektu.
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;
Jeden detail vlastnictví zajišťuje bezpečný úklid. Po vložení patří objekt stránky samotné stránce, nikoli objektu XObject. Pozdější uvolnění XObject nezneplatní umístění, která jste již provedli. To umožňuje fungování pořadí vytvořit-umístit-uvolnit popsaného níže.
Pravidlo životnosti handle, které lidi zaskakuje
S handle XObject se pojí dvě omezení a ignorování kteréhokoli z nich vede k selhání, které se zdá nesouviset se svou příčinou. Za prvé, zdrojový dokument musí být aktivní v okamžiku volání CreateXObjectFromPage. Zachycení čte obsah zdrojové stránky z živého zdrojového dokumentu, takže tento dokument i jeho stránka musí být otevřené a platné při vytváření handle. Za druhé – a to je věc, která lidi překvapuje – handle musí být uvolněn před uzavřením zdrojové stránky a v praxi před uzavřením nebo uvolněním zdrojového dokumentu, ze kterého pochází.
Důvodem je, že XObject je odkazem do struktury, kterou zdrojový dokument stále vlastní. Nejedná se o samostatnou, oddělenou kopii, kterou byste mohli přenášet poté, co zdrojový dokument zanikne. Zavřete-li nejprve zdroj, handle zůstane ukazovat na obsah, který byl odstraněn, takže jeho pozdější uvolnění nebo jakékoli jiné použití pracuje s pamětí, která již není platná. Příznakem je klasický projev visícího ukazatele (dangling handle): chyba porušení přístupu (access violation) při ukončení programu nebo nepravidelná chybovost přesouvající se v závislosti na pořadí alokace s trasou volání ukazující na čisticí kód namísto řádku, který problém skutečně způsobil. Řešením je správné pořadí, nikoli defenzivní programování. Vytvořte XObject, vložte jej na každou stránku, která jej vyžaduje, uvolněte XObject a teprve poté zavřete zdrojový dokument. Destruktor TPdfXObject uvolní podkladový handle PDFium za vás, takže uvolnění obálky ve správný čas je celou vaší odpovědností.
Matice a co znamená jejích šest čísel
Umístění je 2D afinní transformace, stejná, jakou formát PDF používá všude pro umísťování obsahu (ISO 32000-1, sekce 8.3.4). Jedná se o šest čísel zapsaných jako a, b, c, d, e, f a PDFium je zpřístupňuje jako záznam FS_MATRIX. Mapují bod z vlastního prostoru objektu do prostoru stránky:
// 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)
Těchto seš hodnot můžete vyplnit ručně, ale ruční skládání je místem, kde rotace selhává, protože rotace míchá všechny čtyři hodnoty a, b, c, d dohromady. Obálka TPdfMatrix skládá běžné operace za vás a provádí post-násobení za běhu, takže Translate, Scale a Rotate se řetězí v pořadí, v jakém je voláte. Diagonální vodoznak představuje rotaci následovanou posunem pro vystředění; rohové logo je změna měřítka následovaná posunem. Když je matice připravena, předejte její hodnotu volání FPDFPageObj_SetMatrix(PageObj, M.Handle), kde M.Handle je podkladová matice FS_MATRIX. Nízkoúrovňová funkce FPDFPageObj_Transform, která přebírá šest hodnot přímo jako doubles, je k dispozici, pokud raději předáváte čísla než stavíte obalový objekt.
Razítkování každé stránky ve správném pořadí
Celý vzor spojuje jednotlivé části dohromady s pořadím vyžadovaným pravidlem životnosti. Otevřete oba dokumenty, zachyťte razítko jednou, projděte cílové stránky s postupným výběrem každé z nich a vložením plus umístěním kopie, poté uvolněte XObject, uložte soubor a zdrojový dokument nechte zavřít jako poslední.
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;
Tvar bloků try vykonává hlavní práci. Vnitřní finally uvolňuje XObject dříve, než řízení dosáhne vnějšího finally uvolňujícího Stamp, takže handle je vždy uvolněn v době, kdy je zdroj stále živý, a to i v případě, že uprostřed cyklu dojde k výjimce. Dosáhněte tohoto správného zanoření a pravidlo životnosti se vyřeší samo. (Použijte jakýkoli způsob výběru aktuální stránky, který vaše sestavení nabízí; tělo cyklu je stejné.)
Razítkování je jednou z oblastí větší sady nástrojů pro vytváření a úpravu obsahu stránek. Pokud je samotným razítkem obrázek namísto zachycené stránky, převod obrázků do dokumentů PDF s PDFium popisuje nejprve vložení této bitmapy do dokumentu. A když věc, kterou chcete přenášet vedle viditelného razítka, je soubor namísto kresby na stránce, práce s přílohami PDF v Delphi ukazuje stranu vnořených souborů. Vše se dodává s produktem PDFium Component pro Delphi a C++Builder spolu s rozhraními API pro vykreslování, úpravy a dokumenty popsanými na jiných místech tohoto blogu.