Technický článek

Opakovaně použitelná razítka stránky pomocí Form XObjects s PDFium

Razítkování vodoznaku nebo loga na každou stránku dokumentu vypadá jako pětiminutová práce, dokud neotevřete výsledek v inspektoru velikosti souboru. Zřejmým přístupem je projít stránky a na každé z nich znovu vytvořit stejné textové nebo obrazové objekty. To funguje vizuálně, ale je to plýtvání, které se hromadí. Diagonální vodoznak "DRAFT" nakreslený přímo na stostránkovou zprávu je sto kopií stejné cesty a textových dat sedících v tocích obsahu a uložený soubor nese každou z nich

Form XObject je konstrukt, který PDF poskytuje, aby se přesně tomuto zabránilo. Zabalí kus opakovaně použitelného obsahu, celou stránku nebo malou šablonu, do jednoho pojmenovaného objektu, který lze mnohokrát vykreslit na mnoha pozicích. Obsah žije v souboru pouze jednou. Každá stránka, která chce razítko, obsahuje krátkou instrukci, která říká "vykresli XObject N zde, s touto transformací". Stostránkový vodoznak pak do souboru přidá jeden objekt obsahu namísto sta, a to je rozdíl mezi dokumentem, který roste lineárně s počtem stránek, a dokumentem, který tak nečiní. Vodoznaky, razítka loga, šablony čísel stránek a pečeti mají stejný tvar problému a Form XObject je tím správným nástrojem pro každý z nich

Proč jeden uložený objekt poráží sto překreslení

Úspora je strukturální, nikoliv kosmetická. PDF stránka se vykresluje provedením svého toku obsahu (content stream), což je sekvence operátorů kreslení. Když překreslíte razítko na každou stránku, přidáváte celou sekvenci operátorů pro toto razítko do toku každé stránky a byty jsou duplikovány tolikrát, kolik máte stránek. Form XObject přesune tyto operátory do jednoho toku 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

Záleží na tom nejvíce, když je razítko těžké. Vektorová pečeť se stovkami segmentů cest, nebo bitmapové logo, je drahá na uložení. Po uložení a odkazování je těžká část zaplacena pouze jednou a režie na stránku představuje několik bytů vyvolání. Vizuální výsledek na stránce je identický s přímým překreslením, což je účel. Čtenář nepozná rozdíl; velikost souboru však ano

Zachycení stránky do XObject

PDFium buduje opakovaně použitelný objekt ze stávající stránky. Zdrojem je stránka v nějakém dokumentu, který máte otevřený, malý jednostránkový PDF obsahující jen vaši 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, tedy tomu, který razítkujete

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

Signatura je CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda vyvolá výjimku, pokud zdrojový dokument není Active, a místo vyvolání vrací nil, když PDFium nemůže objekt vytvořit, takže výše uvedená explicitní kontrola není volitelná. Vrácené handle je vámi vlastněný TPdfXObject a dvě omezení životnosti k němu připojená jsou tou částí tohoto celého cvičení, která lidi nachytá, takže dostanou svou vlastní sekci níže

Umístění razítka na stránku

Zachycený XObject sám o sobě nedělá nic. Abyste jej zobrazili, vložte jeho kopii na aktuální stránku dokumentu, tu vybranou pomocí vlastnosti PageNumber (číslováno od 1), pomocí InsertFormObjectFromXObject. Toto volání vrátí podkladový objekt stránky, FPDF_PAGEOBJECT, a vrácené handle slouží k napozicování 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 to, co chcete

Protože InsertFormObjectFromXObject vloží jednu kopii za každé volání a pokaždé vrátí nový objekt stránky, můžete vykreslit stejný XObject několikrát na jedné stránce s různými transformacemi a uložený obsah se v souboru stále počítá pouze jednou. Rohové logo a slabý celostránkový vodoznak mohou pocházet ze stejného zachycené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;

Dva úklidové detaily zajišťují, že je to bezpečné. Za prvé, po vložení objekt stránky patří stránce, nikoliv objektu XObject. Pozdější uvolnění XObjectu neznehodnotí umístění, která jste již vytvořili. To umožňuje, aby fungovalo níže popsané pořadí create-place-free (vytvoř-umísti-uvolni). Za druhé, vkládání a pozicování mění pouze seznam objektů stránky v paměti; UpdatePage je to, co serializuje tento seznam zpět do toku obsahu stránky, takže stránka, kterou upravíte bez jejího volání, se uloží tak, jako by razítko nebylo nikdy umístěno

Pravidlo o životnosti handle, které lidi dokáže kousnout

Handle XObjectu řídí dvě omezení a ignorování kteréhokoliv z nich způsobí selhání, které vypadá, že nesouvisí s jeho příčinou. Zaprvé, zdrojový dokument musí být aktivní v okamžiku, kdy zavoláte CreateXObjectFromPage. Zachycení čte obsah zdrojové stránky z živého zdrojového dokumentu, takže tento dokument a jeho stránka musí být otevřené a platné, když je handle budováno. Zadruhé, a to lidi překvapuje, handle musí být uvolněno předtím, než je zdrojová stránka zavřena, a v praxi předtím, než zavřete nebo uvolníte zdrojový dokument, ze kterého pochází

Důvodem je, že XObject je odkazem do struktury, kterou zdrojový dokument stále vlastní. Není to odpojená, soběstačná kopie, kterou byste mohli nosit po zániku zdroje. Pokud nejprve zavřete zdroj, handle zůstane ukazovat na obsah, který byl stržen, takže jeho pozdější uvolnění nebo jakékoliv jiné použití operuje s pamětí, která již není platná. Symptomem je klasický projev pro visící handle (dangling handle): porušení přístupu (access violation) při vypínání nebo občasné poškození, které se přesouvá v závislosti na pořadí alokace, s hromádkou (stack), která ukazuje na čistící kód spíše než na řádek, který skutečně problém způsobil. Řešením je pořadí, nikoliv defenzivní kódování. Sestavte XObject, vložte jej na každou stránku, která jej potřebuje, uvolněte XObject, a až potom zavřete zdrojový dokument. Destruktor TPdfXObject za vás uvolní podkladové handle PDFium, takže uvolnění wrapperu ve správný čas je celá vaše odpovědnost

Matice a co jejích šest čísel znamená

Umístění je 2D afinní transformace, stejná, jakou PDF používá všude pro pozicování obsahu (ISO 32000-1, sekce 8.3.4). Je to šest čísel, zapsaných jako a, b, c, d, e, f, a PDFium je vystavuje 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 šest hodnot můžete vyplnit ručně, ale skládání ručně je místem, kde rotace selhává, protože rotace mísí dohromady všechny čtyři z a, b, c, d. Wrapper TPdfMatrix z jednotky FPdfMatrix pro vás skládá běžné operace a násobí zprava (post-multiplies) za běhu, takže se Translate, Scale a Rotate řetězí v pořadí, ve kterém je voláte. Diagonální vodoznak je rotace následovaná překladem (translate) pro jeho znovuvystředění; rohové logo je měřítko následované překladem. Když je matice připravena, zkopírujte její surovou hodnotu, vlastnost Handle typu FS_MATRIX, do lokální proměnné a předejte ji do FPDFPageObj_SetMatrix; import deklaruje matici jako parametr var, takže vlastnost mu nemůže být předána přímo, a jeho výsledek je 0 při selhání. Nízkoúrovňová funkce FPDFPageObj_Transform, která přijímá šest hodnot přímo jako doubles, je k dispozici, když byste raději předávali čísla, než budovali wrapper

Razítkování každé stránky ve správném pořadí

Celý vzor spojuje kousky s uspořádáním, které vyžaduje pravidlo životnosti. Otevřete oba dokumenty, zachyťte razítko jednou, projděte cílové stránky postupným nastavením vlastnosti PageNumber počínaje od jedničky, a vložením a napozicováním kopie, potvrděním každé stránky pomocí UpdatePage, poté uvolněte XObject, uložte pomocí SaveAs a zdrojový dokument nechejte zavřít jako poslední

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 bloků try dělá skutečnou práci. Vnitřní finally uvolní XObject předtím, než se řízení může vůbec dostat k vnějšímu finally, které uvolní Stamp, takže handle je vždy uvolněno, zatímco jeho zdroj je stále naživu, dokonce i když uprostřed smyčky dojde k výjimce. Získejte správné vnoření a pravidlo o životnosti se postará samo o sebe

Razítkování je jedním z koutů rozsáhlejší sady nástrojů pro tvorbu a úpravu obsahu stránek. Pokud je samotné vaše razítko obrazem (obrázkem) namísto zachycené stránky, článek o převodu obrázků do PDF dokumentů pomocí PDFium pokrývá to, jak tuto bitmapu dostat do dokumentu jako první. A když to, co chcete nést vedle viditelného razítka, je soubor, spíše než inkoust na stránce, článek o práci s přílohami PDF v Delphi ukazuje stránku vestavěného souboru. To vše je dodáváno s komponentou PDFium pro Delphi a C++Builder, spolu s API pro renderování, editaci a API dokumentu probíranými jinde na tomto blogu