Odborný článok

Opakovane použiteľné pečiatky stránok pomocou Form XObjects s PDFium

Razítkovanie vodoznaku alebo loga na každú stránku dokumentu vyzerá ako päťminútová robota, až kým výsledok neotvoríte v inšpektorovi veľkosti súboru. Zjavným prístupom je prechádzať stránky a na každej znova zostaviť tie isté textové alebo obrazové objekty. Vizuálne to funguje, ale je to zbytočné plytvanie spôsobom, ktorý sa sčítava. Diagonálny vodoznak "KONCEPT" nakreslený priamo na stostránkovú správu predstavuje sto kópií tej istej cesty a textových údajov umiestnených v prúdoch obsahu, pričom uložený súbor nesie každú jednu z nich

Form XObject je konštrukt, ktorý PDF poskytuje, aby sa presne tomuto zabránilo. Zabalí časť opakovane použiteľného obsahu, celú stránku alebo malú šablónu, do jedného pomenovaného objektu, ktorý je možné mnohokrát vykresliť na mnohých pozíciách. Obsah žije v súbore iba raz. Každá stránka, ktorá chce pečiatku, obsahuje krátku inštrukciu, ktorá hovorí "tu vykresli XObject N s touto transformáciou". Stostránkový vodoznak potom do súboru pridá jeden objekt obsahu namiesto stovky, a to je rozdiel medzi dokumentom, ktorý rastie lineárne s počtom strán, a takým, ktorý nerastie. Vodoznaky, pečiatky s logom, šablóny čísel strán a plomby sú všetko rovnaký typ problému a Form XObject je tým správnym nástrojom pre každý z nich

Prečo jeden uložený objekt prekoná sto prekrývaní (redraws)

Úspora je štrukturálna, nie kozmetická. Stránka PDF sa vykresľuje spustením svojho toku obsahu (content stream), čo je sekvencia vykresľovacích operátorov. Keď prekreslíte pečiatku pre každú stránku, pridávate úplnú sekvenciu operátorov pre túto pečiatku do toku každej stránky a bajty sa duplikujú toľkokrát, koľko máte strán. Form XObject presunie tieto operátory do jedného toku uloženého v dokumente iba raz. Odkaz (referencia), ktorý si jednotlivá stránka uchováva, je malý: vloží transformačnú maticu, vyvolá XObject a obnoví stav. Počet stránok už nenásobí náklady na grafiku

Toto je najdôležitejšie, keď je pečiatka (stamp) veľká (ťažká). Vektorová plomba so stovkami segmentov ciest alebo bitmapa loga je drahá na ukladanie. Keď sa uloží raz a iba sa na ňu odkazuje, táto ťažká časť sa zaplatí iba raz a réžia na jednu stránku je len niekoľko bajtov na vyvolanie. Vizuálny výsledok na stránke je identický s priamym prekreslením, čo je aj zmyslom. Čitateľ nespozná rozdiel; veľkosť súboru ho však určite spozná

Zachytenie stránky do objektu XObject

PDFium buduje opakovane použiteľný objekt z existujúcej stránky. Zdrojom je stránka v nejakom otvorenom dokumente, malom jednostránkovom PDF, ktoré neobsahuje nič iné, len vašu grafiku pre vodoznak, alebo konkrétna stránka väčšieho súboru. CreateXObjectFromPage zachytí obsah tejto zdrojovej stránky do opakovane použiteľného ovládača (handle), ktorý patrí cieľovému dokumentu – tomu, ktorý práve pečiatkujete

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) ...

Podpis je CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metóda vyvolá výnimku, ak zdrojový dokument nie je Active (aktívny), a vráti nil namiesto vyvolania výnimky, ak PDFium nedokáže objekt zostaviť, takže predchádzajúca explicitná kontrola nie je nepovinná. Ovládač, ktorý sa vráti späť, je TPdfXObject, ktorý vlastníte vy, pričom dve obmedzenia jeho životnosti sú súčasťou celého tohto cvičenia, na ktorom sa ľudia často chytia, a preto majú nižšie svoju vlastnú časť

Umiestnenie pečiatky na stránku

Zachytený XObject sám o sebe nerobí nič. Aby sa objavil, vložíte jeho kópiu na aktuálnu stránku dokumentu, ktorú vyberiete pomocou vlastnosti PageNumber (číslovanou od 1), pomocou funkcie InsertFormObjectFromXObject. Toto volanie vráti podkladový objekt stránky (FPDF_PAGEOBJECT) a práve cez vrátený ovládač určíte jeho polohu pri umiestnení. Bez transformácie dopadne pečiatka do počiatku súradníc zdrojovej stránky, čo je len zriedka tam, kde by ste ju chceli mať

Pretože InsertFormObjectFromXObject vloží jednu kópiu pri každom volaní a zakaždým vráti čerstvý objekt stránky, môžete rovnaký XObject vykresliť na jednej stránke aj viackrát s rôznymi transformáciami a uložený obsah sa v súbore stále započíta iba raz. Rohové logo a jemný celostránkový vodoznak tak môžu pochádzať z rovnakého zachyteného objektu

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;

Bezpečnosť celého procesu zabezpečujú dva organizačné detaily (housekeeping). Po prvé, po vložení patrí objekt stránky samotnej stránke, nie objektu XObject. Neskoršie uvoľnenie XObject-u nespôsobí zneplatnenie (neplatnosť) umiestnení, ktoré ste už vykonali. Práve toto umožňuje fungovanie postupnosti vytvoriť-umiestniť-uvoľniť (create-place-free), opísanej nižšie. Po druhé, vkladanie a polohovanie mení zoznam objektov stránky iba v pamäti; UpdatePage je to, čo tento zoznam serializuje späť do toku obsahu stránky, takže stránka, ktorú upravíte bez jeho zavolania, sa uloží tak, akoby pečiatka nebola nikdy umiestnená

Pravidlo životnosti (lifetime rule) ovládačov, ktoré dokáže potrápiť

Ovládač XObject riadia dve obmedzenia a ignorovanie ktoréhokoľvek z nich vedie k zlyhaniu, ktoré navonok nesúvisí s jeho skutočnou príčinou. Prvým je, že zdrojový dokument musí byť aktívny v okamihu, keď zavoláte CreateXObjectFromPage. Zachytenie načíta obsah zdrojovej stránky zo živého zdrojového dokumentu, takže tento dokument a jeho stránka musia byť otvorené a platné, keď sa buduje ovládač. Po druhé, a toto je to, čo ľudí prekvapí najčastejšie, ovládač musí byť uvoľnený predtým, než sa zatvorí zdrojová stránka, v praxi teda predtým, než zatvoríte alebo uvoľníte zdrojový dokument, z ktorého pochádza

Dôvodom je, že XObject je referenciou do štruktúry, ktorú zdrojový dokument stále vlastní. Nejde o oddelenú a sebestačnú kópiu, ktorú by ste si mohli voľne prenášať po odstránení zdroja. Ak zatvoríte zdroj ako prvý, ovládač zostane odkazovať na obsah, ktorý už bol odstránený z pamäte, takže jeho neskoršie uvoľnenie alebo akékoľvek iné použitie bude operovať nad pamäťou, ktorá už nie je platná. Príznakom je klasické správanie visiaceho (dangling) ukazovateľa: narušenie prístupu k pamäti (access violation) pri vypínaní alebo náhodné poškodenia, ktoré sa objavujú v závislosti od poradia alokácie, pričom zásobník ukazuje na kód upratovania (cleanup) a nie na riadok, ktorý problém skutočne spôsobil. Opravou je dodržanie správneho poradia, nie defenzívne programovanie. Zostavte XObject, vložte ho na každú stránku, ktorá ho potrebuje, uvoľnite XObject a až potom zatvorte zdrojový dokument. Deštruktor pre TPdfXObject za vás automaticky uvoľní podkladový ovládač z PDFium, takže uvoľnenie tohto wrappera v správnom čase je vašou jedinou zodpovednosťou

Matica a čo znamená jej šesť čísel

Umiestnenie tvorí 2D afinná transformácia, teda tá istá, akú PDF formát využíva všade pre polohovanie obsahu (ISO 32000-1, časť 8.3.4). Pozostáva zo šiestich čísel, zapisovaných ako a, b, c, d, e, f, a PDFium ich sprístupňuje ako záznam FS_MATRIX. Tie mapujú bod z vlastného priestoru objektu do priestoru 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 šesť hodnôt môžete vyplniť ručne, no práve manuálne zostavovanie je tým momentom, kedy sa pri rotácii často robia chyby, pretože rotácia zlučuje všetky štyri hodnoty z a, b, c, d dokopy. Obalová trieda (wrapper) TPdfMatrix z jednotky FPdfMatrix, pre vás komponuje bežné operácie a násobí ich z pravej strany za pochodu (post-multiplication), takže operácie Translate, Scale a Rotate sa zreťazia presne v takom poradí, v akom ich voláte. Diagonálny vodoznak znamená rotáciu nasledovanú posunutím na opätovné vycentrovanie; rohové logo je zmena mierky (škály) nasledovaná posunutím (translate). Keď je matica pripravená, prekopírujte jej priamu (raw) hodnotu z vlastnosti Handle typu FS_MATRIX do lokálnej premennej a tú odovzdajte do funkcie FPDFPageObj_SetMatrix; import totiž deklaruje maticu ako var parameter, takže vlastnosť (property) sa do nej nedá odovzdať priamo a výsledkom by v prípade zlyhania bola hodnota 0. Alternatívne je k dispozícii funkcia nižšej úrovne s názvom FPDFPageObj_Transform, ktorá priamo preberá šesť hodnôt ako parametre typu double, ak by ste chceli pracovať radšej priamo s číslami než by ste stavali celú wrapperovú (obalovú) štruktúru

Pečiatkovanie každej stránky v správnom poradí

Úplný vzor skladá jednotlivé kúsky dohromady s usporiadaním, ktoré si vyžaduje pravidlo životnosti (lifetime rule). Otvorte obidva dokumenty, raz zachyťte pečiatku, postupne prejdite cieľové stránky (destination pages) po jednej tým, že upravíte vlastnosť PageNumber zakaždým priradením od 1 po koniec, pridajte kópiu a stanovte jej transformáciu. Vytvrdenie úprav každej stránky zavoláte funkciou UpdatePage, následne ihneď uvoľnite prvok pre pečiatku XObject a zápis zavŕšte funkciou SaveAs. Tým pádom na záver necháte zdrojový dokument bezpečne a úplne nakoniec zatvoriť

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;

Tvar a usporiadanie (nesting) v blokoch try odvádzajú skutočnú prácu. Vnútorný blok finally uvoľňuje objekt XObject ešte predtým, ako by sa kedykoľvek mohol dostať priebeh (control) k vonkajšiemu bloku finally, ktorý nakoniec oslobodzuje ovládač Stamp. S týmto zabezpečením sa parameter XObject (jej handle pre PDFium) uvoľní vtedy, keď zdroj je stále nažive – to chráni fungovanie aj v stave chybovej výnimky uprostred slučky. Dodržaním takého vnárania spĺňate podmienky pravidla životnosti a proces sa o seba postará v úplnom znení

Pečiatkovanie stránok formátu (stamping) reprezentuje iba malú vetvu v komplexnej výbave na vizuálnu štruktúrovanú tvorbu a zmeny obsahu. Niekedy sa však vodotlače alebo vodoznaky v podobe vkladaného objektu týkajú iba priameho presunu vkladaného vizuálu (ako samotnej bitmapovej ilustrácie a nie zachytávania stránok do šablón) – v takom prípade tému vkladania grafických príloh obsiahla podstata príspevku určená pre konverziu obrázkov do PDF pomocou PDFium s detailom nasadenia bitových mapových súborov. Keď vec pripájaná popri vytvorenom obryse nepožaduje prístup pomocou vizualizácie nákresu a skôr odkazuje priamo na súbor umiestnený zvnútra formátu PDF, práca s prílohami v PDF pomocou Delphi predstavuje časť slúžiacu výhradne pre priradenia dát a súborov do celku. Výbava pochádza ako kompaktný celok spracovaná na prítomnosť pre funkčné vlastnosti u voľby a zostavy s prístupom na modul pre PDFium Component dodávaným na Delphi a rovnako i C++Builder v tandeme tvoriacom všetky voľby API spomenuté v štruktúre tohto blogu zameraného na dokumentovú vrstvu, vykresľovanie obsahu a upravovanie štruktúr stránky