Egy dokumentum minden oldalára vízjelet vagy logót bélyegezni (stamping) ötperces munkának tűnik, amíg meg nem nyitja az eredményt egy fájlméret-vizsgálóban. A kézenfekvő megközelítés az, hogy végigmegy az oldalakon, és mindegyiken újra felépíti ugyanazokat a szöveg- vagy képobjektumokat. Ez vizuálisan működik, de halmozottan pazarló. Egy százoldalas jelentésre közvetlenül rajzolt átlós „DRAFT” (piszkozat) vízjel ugyanazon útvonal (path) és szövegadat száz példányát jelenti a tartalomfolyamokban (content streams), és a mentett fájl mindegyiket hordozza
A Form XObject az a szerkezet, amelyet a PDF pontosan ennek elkerülésére biztosít. Egy darab újrahasználható tartalmat – egy teljes oldalt vagy egy kis sablont – egyetlen megnevezett objektumba csomagol, amely többször és több pozícióban is megfesthető. A tartalom egyszer él a fájlban. Minden oldal, amely a bélyegzőt kéri, egy rövid utasítást tartalmaz: „fesd ide az N XObject-et ezzel a transzformációval”. Egy százoldalas vízjel így egyetlen tartalomobjektumot ad a fájlhoz száz helyett, és ez a különbség a lineárisan az oldalszámmal növekvő és az azzal nem növekvő dokumentum között. A vízjelek, logóbélyegzők, oldalszámsablonok és pecsétek (seals) mind azonos jellegű problémák, és a Form XObject a megfelelő eszköz mindegyikhez
Miért jobb egyetlen tárolt objektum, mint száz újra-rajzolás
A megtakarítás strukturális, nem pedig kozmetikai. Egy PDF oldal a tartalomfolyamának (content stream) – egy rajzoló operátorok sorozatának – végrehajtásával jelenik meg (renders). Amikor oldalanként újrarajzol egy bélyegzőt, a bélyegző teljes operátorsorozatát hozzáfűzi (appending) minden egyes oldal folyamához, és a bájtok annyiszor duplikálódnak, ahány oldala van. Egy Form XObject ezeket az operátorokat a dokumentumban egyszer tárolt egyetlen folyamba helyezi át. Az az egyedi hivatkozás, amelyet egy oldal megtart, kicsi: feltesz (pushes) egy transzformációs mátrixot, meghívja az XObject-et, és helyreállítja (restores) az állapotot. Az oldalszám többé nem többszörözi meg a grafika (artwork) költségét
Ez leginkább akkor számít, ha a bélyegző nehéz. Egy több száz útvonal-szegmensből (path segments) álló vektoros pecsét (seal) vagy egy logó bittérkép tárolása drága. Egyszer tárolva és hivatkozva a nehéz részért csak egyszer fizetünk, és az oldalankénti többletköltség (overhead) mindössze néhány bájtnyi hívás. A vizuális eredmény az oldalon megegyezik a közvetlen újrarajzolással, ami a lényeg. Az olvasó nem tud különbséget tenni; a fájlméret viszont nagyon is
Oldal rögzítése egy XObject-be
A PDFium az újrahasználható objektumot egy meglévő oldalból építi fel. A forrás egy nyitott dokumentum egyik oldala, egy kis egyoldalas PDF, amely nem tartalmaz mást, csak a vízjel grafikáját, vagy egy nagyobb fájl egy adott oldala. A CreateXObjectFromPage rögzíti (captures) e forrásoldal tartalmát egy újrahasználható kezelőbe (handle), amely a céldokumentumhoz tartozik, ahhoz, amelyre a bélyegzőt helyezi
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) ...
A szignatúra CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. A metódus kivételt dob (raises), ha a forrásdokumentum nem Active, és nil-t ad vissza kivételdobás helyett, ha a PDFium nem tudja felépíteni az objektumot, így a fenti explicit ellenőrzés nem opcionális. A visszakapott kezelő (handle) egy olyan TPdfXObject, amelyet Ön birtokol, és a hozzá tartozó két élettartam-korlátozás az a része ennek az egész gyakorlatnak, amelyen az emberek elcsúsznak, ezért lentebb egy külön szakaszt kapnak
A bélyegző elhelyezése az oldalon
Egy rögzített XObject önmagában semmit sem csinál. Ahhoz, hogy megjelenjen, be kell illesztenie (insert) egy másolatát a dokumentum aktuális oldalára – amelyet az 1-alapú PageNumber tulajdonság választ ki – az InsertFormObjectFromXObject segítségével. Ez a hívás visszaadja a mögöttes oldalobjektumot (egy FPDF_PAGEOBJECT-et), és a visszakapott kezelő (handle) az, amivel pozícionálja az elhelyezést. Transzformáció nélkül a bélyegző az origónál, a forrásoldal saját koordinátái szerint landol, ami ritkán az a hely, ahová szánja
Mivel az InsertFormObjectFromXObject hívásonként egy másolatot szúr be, és minden alkalommal egy friss oldalobjektumot ad vissza, ugyanazt az XObject-et többször is megfestheti egy oldalon különböző transzformációkkal, miközben a tárolt tartalom továbbra is csak egyszer számít a fájlban. Egy sarki logó és egy halvány, teljes oldalas vízjel is származhat ugyanabból a rögzített objektumból
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;
Két háttérmunka (housekeeping) részlet teszi ezt biztonságossá. Először, a beillesztés után az oldalobjektum az oldalhoz tartozik, nem az XObject-hez. Az XObject későbbi felszabadítása nem érvényteleníti a már elvégzett elhelyezéseket. Ez az, ami lehetővé teszi az alább leírt „létrehozás-elhelyezés-felszabadítás” sorrend (create-place-free ordering) működését. Másodszor, a beillesztés és a pozicionálás csak a memóriában lévő oldalobjektum-listát változtatja meg; az UpdatePage az, amely vissza-sorosítja (serialises) ezt a listát az oldal tartalomfolyamába (content stream), így egy olyan oldal, amelyet e nélkül a hívás nélkül szerkeszt, úgy mentődik el, mintha a bélyegzőt soha nem helyezték volna el rajta
A kezelő élettartamának szabálya, amely sokak számára buktató
Két korlátozás vonatkozik az XObject kezelőre (handle), és bármelyikük figyelmen kívül hagyása olyan hibát eredményez, amely látszólag nem kapcsolódik az okához. Először is, a forrásdokumentumnak aktívnak kell lennie a CreateXObjectFromPage meghívásának pillanatában. A rögzítés kiolvassa a forrásoldal tartalmát az élő forrásdokumentumból, így annak a dokumentumnak és az oldalának nyitottnak és érvényesnek kell lennie, amikor a kezelő létrejön. Másodszor – és ez az, ami meglepi az embereket –, a kezelőt fel kell szabadítani a forrásoldal bezárása előtt, a gyakorlatban pedig mielőtt bezárná vagy felszabadítaná azt a forrásdokumentumot, amelyből származik
Az ok az, hogy az XObject egy olyan struktúrára mutató hivatkozás, amelyet a forrásdokumentum még mindig birtokol. Ez nem egy leválasztott (detached), önálló másolat, amelyet hordozhat a forrás megszűnése után is. Ha előbb bezárja a forrást, a kezelő egy már lebontott (torn down) tartalomra mutat, így későbbi felszabadítása, vagy bármilyen más felhasználása érvénytelen memórián fog operálni. A tünet a klasszikus „lógó mutató” (dangling handle) tünet: hozzáférési hiba (access violation) leállításkor, vagy a lefoglalási (allocation) sorrendtől függően vándorló, időszakos memóriasérülés, amelynek a hívási verme (stack) a takarítási kódra mutat, nem pedig arra a sorra, amely valójában a problémát okozta. A megoldás a sorrend (ordering), nem a defenzív kódolás. Építse fel az XObject-et, illessze be minden olyan oldalra, amelynek szüksége van rá, szabadítsa fel az XObject-et, és csak ezután zárja be a forrásdokumentumot. A TPdfXObject destruktora felszabadítja Ön helyett a mögöttes PDFium kezelőt (handle), így a burkoló (wrapper) megfelelő időben történő felszabadítása a teljes felelőssége
A mátrix, és mit jelent a hat száma
Az elhelyezés egy 2D affin transzformáció (affine transform), ugyanaz, amelyet a PDF mindenhol használ a tartalom pozicionálására (ISO 32000-1, 8.3.4. szakasz). Hat számból áll, amelyeket a, b, c, d, e, f formában írnak, és a PDFium ezeket FS_MATRIX rekordként teszi elérhetővé. Egy pontot képeznek le az objektum saját teréből az oldaltérbe (page space):
// 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)
Kitöltheti ezt a hat értéket kézzel, de a kézzel történő összeállításuknál szokott elromlani az elforgatás (rotation), mivel a forgatás az a, b, c, d mind a négy értékét összekeveri. A TPdfMatrix burkoló (wrapper), az FPdfMatrix unit-ból, összeállítja Ön helyett a gyakori műveleteket, és utószorozással (post-multiplies) operál, így a Translate (eltolás), Scale (méretezés) és Rotate (forgatás) abban a sorrendben láncolódik (chain), ahogyan meghívja őket. Egy átlós vízjel egy elforgatás (rotate), majd egy eltolás (translate) az újra-központosításához (recentre); egy sarki logó egy méretezés (scale), majd egy eltolás. Amikor a mátrix elkészült, másolja be a nyers értékét (az FS_MATRIX típusú Handle tulajdonságot) egy helyi (local) változóba, és adja át az FPDFPageObj_SetMatrix függvénynek; az importálás a mátrixot var paraméterként deklarálja, így egy tulajdonságot nem lehet közvetlenül átadni neki, eredménye hiba esetén pedig 0. Az alacsonyabb szintű FPDFPageObj_Transform, amely a hat értéket közvetlenül double-ként veszi fel, akkor érhető el, ha inkább számokat adna át ahelyett, hogy burkolót építene
Minden oldal megjelölése a megfelelő sorrendben
A teljes minta úgy rakja össze a darabokat, ahogyan azt az élettartamra vonatkozó szabály (lifetime rule) sorrendje megköveteli. Nyissa meg mindkét dokumentumot, rögzítse egyszer a bélyegzőt, menjen végig a céloldalakon az 1-alapú PageNumber beállításával, szúrjon be (inserting) és pozicionáljon egy másolatot, hagyja jóvá (committing) az egyes oldalakat az UpdatePage hívással, majd szabadítsa fel az XObject-et, ezután mentsen a SaveAs-zel, és végül engedje a forrásdokumentumot utolsóként bezáródni
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;
A try blokkok formája végzi az igazi munkát. A belső finally felszabadítja az XObject-et, mielőtt a vezérlés egyáltalán elérhetné a külső finally blokkot, amely a Stamp-et szabadítja fel, így a kezelő (handle) mindig akkor enged el, amíg a forrása még él, még akkor is, ha egy kivétel (exception) sül el a ciklus közepén. Állítsa be jól ezt az egymásba ágyazást (nesting), és az élettartam-szabály gondoskodik önmagáról
A bélyegzés egy nagyobb eszköztár egy kis része az oldaltartalom felépítés és szerkesztéséhez. Ha maga a bélyegző egy kép, és nem egy rögzített oldal, akkor a képek konvertálása PDF dokumentumokká PDFium segítségével című rész foglalkozik azzal, hogyan juttassa be azt a bittérképet (bitmap) először egy dokumentumba. És amikor az a dolog, amit a látható bélyegző mellett hordozni szeretne, egy fájl, nem pedig tinta (ink) az oldalon, a PDF mellékletek kezelése Delphiben cikk megmutatja a beágyazott-fájl (embedded-file) oldalát. Mindez a Delphihez és C++Builderhez készült PDFium Component csomag részeként érhető el, a megjelenítési, szerkesztési és dokumentum API-k mellett, amelyekről a blog többi része szól