Technical Article

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

Opečiatkovanie každej stránky dokumentu vodotlačou alebo logom vyzerá ako päťminútová práca, kým neotvoríte výsledok inšpektora veľkosti súborov. Zrejmým prístupom je prejsť stránky a na každej z nich znova zostaviť tie isté textové alebo obrázkové objekty. To síce funguje vizuálne, ale je to neefektívne spôsobom, ktorý sa sčítava. Diagonálna vodotlač "DRAFT" nakreslená priamo na stostránkový report predstavuje sto kópií rovnakej cesty a textových dát sediacich v tokoch obsahu a 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é vykresliť viackrát na rôznych pozíciach. Obsah žije v súbore iba raz. Každá stránka, ktorá vyžaduje pečiatku, obsahuje krátku inštrukciu hovoriacu "vykresli XObject N tu, s touto transformáciou". Stostránková vodotlač potom pridá do súboru jeden objekt obsahu namiesto stovky, a to je rozdiel medzi dokumentom, ktorý rastie lineárne s počtom stránok, a dokumentom, ktorý nie. Vodotlače, pečiatky s logom, šablóny čísel stránok a pečate sú všetky rovnakým problémom a Form XObject je správnym nástrojom pre každý z nich.

Prečo jeden uložený objekt prekonáva sto prekreslení

Úspora je štrukturálna, nie kozmetická. Stránka PDF sa vykresľuje vykonaním jej toku obsahu (content stream), sekvencie kresliacich operátorov. Keď prekresľujete pečiatku na každú stránku, pripájate celú sekvenciu operátorov pre túto pečiatku k toku každej stránky a bajty sa duplikujú toľkokrát, koľko máte stránok. Form XObject presúva tieto operátory do jedného toku uloženého v dokumente iba raz. Referenčný odkaz, ktorý si jednotlivá stránka uchováva, je malý: vloží maticu transformácie, vyvolá XObject a obnoví stav. Počet stránok už nenásobí cenu grafiky.

Na tomto záleží najviac vtedy, keď je pečiatka ťažká. Vektorová pečať so stovkami segmentov cesty alebo bitmapa loga sú náročné na uloženie. Po uložení raz a odkázaní sa za ťažkú časť zaplatí iba raz a réžia na stránku predstavuje niekoľko bajtov volania. Vizuálny výsledok na stránke je identický s priamym prekreslením, čo je hlavným zmyslom. Čitateľ nespozná rozdiel; veľkosť súboru áno.

Zachytenie stránky do XObjectu

PDFium zostavuje opakovane použiteľný objekt z existujúcej stránky. Zdrojom je stránka v nejakom dokumente, ktorý máte otvorený, malé jednostránkové PDF, ktoré neobsahuje nič iné ako grafiku vašej vodotlače, alebo konkrétna stránka väčšieho súboru. CreateXObjectFromPage zachytí obsah tejto zdrojovej stránky do opakovane použiteľného handlera, ktorý patrí cieľovému dokumentu, do ktorého dávate pečiatku.

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

Signatúra je CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metóda vracia nil pri zlyhaní, namiesto vyvolania výnimky, takže explicitná kontrola vyššie nie je voliteľná. Handler, ktorý sa vráti, je TPdfXObject, ktorý vlastníte, a dve obmedzenia životnosti s ním spojené sú časťou tohto celého cvičenia, ktorá ľudí zaskočí, preto majú nižšie svoju vlastnú sekciu.

Umiestnenie pečiatky na stránku

Zachytený XObject sám o sebe nerobí nič. Aby sa zobrazil, vložíte jeho kópiu na aktuálnu stránku dokumentu pomocou InsertFormObjectFromXObject. Toto volanie vráti podkladový objekt stránky, FPDF_PAGEOBJECT, a vrátený handler je spôsobom, akým umiestnite pozíciu. Bez transformácie pečiatka pristane na začiatku súradnicovej sústavy vo vlastných súradniciach zdrojovej stránky, čo je len zriedka tam, kde ju chcete.

Keďže InsertFormObjectFromXObject vkladá jednu kópiu na volanie a zakaždým vracia čerstvý objekt stránky, môžete rovnaký XObject vykresliť na jednej stránke viackrát pri rôznych transformáciach a uložený obsah sa v súbore stále počíta iba raz. Rohové logo a jemná celostránková vodotlač môžu pochádzať z rovnakého zachytené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 vlastníctva robí čistenie bezpečným. Po vložení patrí objekt stránky stránke, nie XObjectu. Neskoršie uvoľnenie XObjectu nezneplatní umiestnenia, ktoré ste už urobili. To umožňuje, aby fungovalo poradie vytvoriť-umiestniť-uvoľniť (create-place-free) opísané nižšie.

Pravidlo životnosti handlera, ktoré ľudí prekvapí

Dve obmedzenia riadia handler XObjectu a ignorovanie ktoréhokoľvek z nich vedie k zlyhaniu, ktoré vyzerá nesúvisiace so svojou príčinou. Po prvé, zdrojový dokument musí byť aktívny v momente volania CreateXObjectFromPage. Zachytenie číta obsah zdrojovej stránky zo živého zdrojového dokumentu, takže tento dokument a jeho stránka musia byť otvorené a platné pri vytváraní handlera. Po druhé, a to je to, čo ľudí prekvapuje, handler must byť uvoľnený predtým, ako sa zdrojová stránka zatvorí, a v praxi predtým, ako zatvoríte alebo uvoľníte zdrojový dokument, z ktorého pochádza.

Dôvodom je, že XObject je referenčným odkazom do štruktúry, ktorú zdrojový dokument stále vlastní. Nie je to oddelená, samostatná kópia, ktorú môžete nosiť so sebou po tom, čo zdroj zmizne. Zatvorte najprv zdroj a handler zostane ukazovať na obsah, ktorý bol zničený, takže jeho neskoršie uvoľnenie, alebo akékoľvek iné použitie, pracuje s pamäťou, ktorá už nie je platná. Príznakom je klasický prípad pre visiaci handler (dangling handle): porušenie prístupu (access violation) pri vypínaní alebo občasné poškodenie dát, ktoré sa presúva v závislosti od poradia alokácie, s call stackom ukazujúcim na kód čistenia a nie na riadok, ktorý problém reálne spôbil. Riešením je poradie krokov, nie defenzívne kódovanie. Zostavte XObject, vložte ho na každú stránku, ktorá ho potrebuje, uvoľnite XObject a až potom zatvorte zdrojový dokument. Destruktor TPdfXObject uvoľní podkladový handler PDFium za vás, takže uvoľnenie obalu v správnom čase je celou vašou zodpovednosťou.

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

Umiestnenie je 2D afinná transformácia, rovnaká, akú PDF používa všade na určovanie polohy obsahu (ISO 32000-1, časť 8.3.4). Ide o šesť čísel, zapisovaných ako a, b, c, d, e, f, a PDFium ich sprístupňuje ako záznam FS_MATRIX. 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, ale ich ručné skladanie je miestom, kde rotácia zlyháva, pretože rotácia mieša všetky štyri hodnoty a, b, c, d dohromady. Obal TPdfMatrix skladá bežné operácie za vás a priebežne ich násobí zozadu (post-multiplication), takže Translate, Scale a Rotate sa reťazia v poradí, v akom ich voláte. Diagonálna vodotlač je rotácia nasledovaná posunom (translate) na jej opätovné vycentrovanie; rohové logo je zmena mierky (scale) nasledovaná posunom. Keď je matica pripravená, odovzdajte jej surovú hodnotu metóde FPDFPageObj_SetMatrix(PageObj, M.Handle), kde M.Handle je podkladová FS_MATRIX. Nižšia úroveň FPDFPageObj_Transform, ktorá berie šesť hodnôt priamo ako čísla s dvojiou presnosťou (doubles), je k dispozícii, ak by ste radšej odovzdali čísla namiesto vytvárania obalu.

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

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 blokov try vykonáva skutočnú prácu. Vnútorná časť finally uvoľní XObject predtým, ako riadenie vôbec dosiahne vonkajšiu časť finally, ktorá uvoľní Stamp, sože handler sa vždy uvoľní, kým je jeho zdroj stále nažive, a to aj vtedy, ak uprostred cyklu dôjde k výnimke. Správne vnorenie zaistí, že pravidlo životnosti sa vyrieši samo. (Použite akýkoľvek volič aktuálnej stránky, ktorý váš build odkrýva; telo cyklu je v oboch prípadoch rovnaké.)

Pečiatkovanie je jednou časťou väčšieho nástroja na vytváranie a úpravu obsahu stránok. Ak je vaša pečiatka samotným obrázkom a nie zachytenou stránkou, článok konverzia obrázkov do PDF dokumentov s PDFium pokrýva najprv dostať túto bitmapu do dokumentu. A keď vec, ktorú chcete prenášať vedľa viditeľnej pečiatky, je súbor a nie atrament na stránke, článok práca s PDF prílohami v Delphi ukazuje stranu vnorených súborov. To všetko sa dodáva s PDFium Component pre Delphi a C++Builder spolu s API pre rendering, úpravy a dokumenty popísanými inde na tomto blogu.