Technical Article

Genanvendelige sidestempler via Form XObjects med PDFium

At stemple et vandmærke eller et logo på hver side i et dokument ligner en fem minutters opgave, indtil du åbner resultatet i en filstørrelsesinspektør. Den oplagte tilgang er at gennemgå siderne og på hver enkelt bygge de samme tekst- eller billedobjekter igen. Det fungerer visuelt, men det er spild på en måde, der akkumuleres. Et diagonalt "DRAFT"-vandmærke tegnet direkte på en rapport på hundrede sider er hundrede kopier af de samme sti- og tekstdata, der ligger i indholdsstrømmene, og den gemte fil bærer hver eneste af dem.

Et Form XObject er den konstruktion, PDF tilbyder for at undgå netop dette. Det pakker et stykke genanvendeligt indhold, en hel side eller en lille skabelon, ind i et enkelt navngivet objekt, der kan tegnes mange gange på mange positioner. Indholdet lever i filen én gang. Hver side, der ønsker stemplet, indeholder en kort instruktion, der siger "tegn XObject N her med denne transformation". Et vandmærke på hundrede sider tilføjer derefter ét indholdsobjekt til filen i stedet for hundrede, og det er forskellen på et dokument, der vokser lineært med sit sidetal, og et, der ikke gør. Vandmærker, logostempler, sidetalsskabeloner og segl er alle det samme problem, og Form XObject er det rigtige værktøj til dem alle.

Hvorfor ét gemt objekt slår hundrede nystegninger

Besparelsen er strukturel, ikke kosmetisk. En PDF-side renderes ved at udføre dens indholdsstrøm, en sekvens af tegneoperatorer. Når du tegner et stempel igen pr. side, tilføjer du den fulde operatorsekvens for det stempel til hver sides strøm, og bytes duplikeres lige så mange gange, som du har sider. Et Form XObject flytter disse operatorer over i én strøm, der gemmes én gang i dokumentet. Den reference, en enkelt side beholder, er lille: Den skubber en transformationsmatrix, kalder XObject'et og gendanner statusen. Sidetallet multiplicerer ikke længere prisen for grafikken.

Dette betyder mest, når stemplet er tungt. Et vektorsegl med hundredvis af stisegmenter eller et logo-bitmap er dyrt at gemme. Gemt én gang og refereret, betales den tunge del en enkelt gang, og overhead pr. side er et par byte til kaldet. Det visuelle resultat på siden er identisk med en direkte nystegning, hvilket er pointen. Læseren kan ikke se forskel; det kan filstørrelsen i høj grad.

Optagelse af en side i et XObject

PDFium bygger det genanvendelige objekt ud fra en eksisterende side. Kilden er en side i et dokument, du har åbent, en lille ensidet PDF, der kun indeholder dit vandmærkegrafik, eller en bestemt side i en større fil. CreateXObjectFromPage optager kildesidens indhold i et genanvendeligt handle, der tilhører destinationsdokumentet – det, du stempler.

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

Placering af stemplet på en side

Et optaget XObject gør intet i sig selv. For at få det til at blive vist indsætter du en kopi af det på dokumentets aktuelle side med InsertFormObjectFromXObject. Det kald returnerer det underliggende sideobjekt, en FPDF_PAGEOBJECT, og det returnerede handle er den måde, du styrer placeringen på. Uden en transformation lander stemplet i startpunktet (origin) i kildesidens egne koordinater, hvilket sjældent er der, du ønsker det.

Fordi InsertFormObjectFromXObject indsætter én kopi pr. kald og returnerer et nyt sideobjekt hver gang, kan du tegne det samme XObject flere gange på én side ved forskellige transformationer, og det gemte indhold tælles stadig kun én gang i filen. Et hjørnelogo og et svagt vandmærke på hele siden kan komme fra det samme optagne objekt.

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;

Reglen om handle-levetid, der snyder folk

To begrænsninger styrer XObject-handlet, og ignorering af en af dem producerer en fejl, der ser ud til at være uden forbindelse til sin årsag. For det første skal kildedokumentet være aktivt i det øjeblik, du kalder CreateXObjectFromPage. Optagelsen læser kildesidens indhold fra det aktive kildedokument, så det dokument og dets side skal være åbne og gyldige, når handlet bygges. For det andet – og det er det, der overrasker folk – skal handlet frigøres, før kildesiden lukkes, og in præksis før du lukker eller frigør det kildedokument, det kom fra.

Årsagen er, at XObject'et er en reference ind i en struktur, som kildedokumentet still owns. Det er ikke en løsrevet, selvstændig kopi, du kan bære rundt på, efter at kilden er væk. Luk kilden først, og handlet efterlades pegende på indhold, der er blevet revet ned, så frigørelse af det senere eller enhver anden brug af det opererer på hukommelse, der ikke længere er gyldig. Symptomet er det klassiske for et dinglende handle: en access-violation ved lukning eller periodisk korruption, der flytter sig afhængigt af allokeringsrækkefølge, med en stak (stack), der peger på oprydningskode frem for på den linje, der rent faktisk forårsagede problemet. Løsningen er rækkefølge, ikke defensiv kodning. Byg XObject'et, indsæt det på hver side, der har brug for det, frigør XObject'et, og luk først derefter kildedokumentet. TPdfXObject-destruktoren frigiver det underliggende PDFium-handle for dig, så frigørelse af wrapperen på det rigtige tidspunkt er hele dit ansvar.

Matrixen, og hvad dens seks tal betyder

Placering er en 2D-affin transformation, den samme som PDF bruger overalt til placering af indhold (ISO 32000-1, afsnit 8.3.4). Det er seks tal, skrevet a, b, c, d, e, f, og PDFium eksponerer dem som FS_MATRIX-posten. De afbilder et punkt fra objektets eget rum til siderummet:

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

Du kan udfylde disse seks værdier i hånden, men at sammensætte dem manuelt er der, hvor rotation går galt, fordi rotation blander alle fire af a, b, c, d sammen. TPdfMatrix-wrapperen sammensætter de almindelige operationer for dig og post-multiplicerer undervejs, så Translate, Scale og Rotate kædes i den rækkefølge, du kalder dem. Et diagonalt vandmærke er en rotation efterfulgt af en translation for at recentrere det; et hjørnelogo er en skalering efterfulgt af en translation. Når matrixen er klar, overfører du dens rå værdi til FPDFPageObj_SetMatrix(PageObj, M.Handle), hvor M.Handle is den underliggende FS_MATRIX. Den lavere FPDFPageObj_Transform, som tager de seks værdier direkte som doubles, er tilgængelig, hvis du hellere vil overføre tal end at bygge en wrapper.

Stempling af hver side i den rigtige rækkefølge

Det fulde mønster samler delene med den rækkefølge, som levetidsreglen kræver. Åbn begge dokumenter, optag stemplet én gang, gennemgå destinationssiderne ved at vælge hver enkelt efter tur og indsætte samt placere en kopi, frigør derefter XObject'et, gem derefter, og lad kildedokumentet lukke til sidst.

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;

Formen på try-blokkene udfører det reelle arbejde. Den indre finally frigør XObject'et, før kontrollen overhovedet kan nå den ydre finally, der frigør Stamp, så handlet altid frigives, mens dets kilde stadig er i live, selvom en undtagelse udløses midt i løkken. Få den indlejring rigtig, og levetidsreglen klarer sig selv. (Brug den vælger for aktuel side, som dit build eksponerer; løkkens krop er den samme uanset hvad).

Stempling er et hjørne af et større værktøjssæt til at opbygge og redigere sideindhold. Hvis dit stempel i sig selv er et billede frem for en optaget side, dækker konvertering af billeder til PDF-dokumenter med PDFium, hvordan du får det bitmap ind i et dokument først. Og når det, du ønsker at transportere ved siden af det synlige stempel, er en fil frem for blæk på siden, viser arbejdet med PDF-vedhæftninger i Delphi siden med indlejrede filer. Alt dette leveres med PDFium Component til Delphi og C++Builder sammen med API'erne til rendering, redigering og dokumenter, der er dækket andre steder på denne blog.