Műszaki cikk

Újrahasználható oldalbélyegzők Form XObject-eken keresztül PDFium-mal

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