Technical Article

Återanvändbara sidstämplar via Form XObjects med PDFium

Att stämpla en vattenstämpel eller en logotyp på varje sida i ett dokument ser ut som ett femminutersjobb tills du öppnar resultatet i en filstorleksinspektör. Det uppenbara tillvägagångssättet är att vandra genom sidorna och på var och en bygga samma text- eller bildobjekt igen. Det fungerar visuellt, men det är slösaktigt på ett sätt som växer. En diagonal "UTKAST"-vattenstämpel ritad direkt på en hundrasidors rapport är hundra kopior av samma bana och textdata som ligger i innehållsströmmarna, och den sparade filen bär på varenda en av dem.

Ett Form XObject är den konstruktion som PDF tillhandahåller för att undvika exakt detta. Det omsluter en del av återanvändbart innehåll, en hel sida eller en liten mall, till ett enda namngivet objekt som kan målas många gånger vid många positioner. Innehållet lever i filen en gång. Varje sida som vill ha stämpeln har en kort instruktion som säger "måla XObject N här, med denna transformation." En hundrasidors vattenstämpel lägger då till ett innehållsobjekt till filen snarare än hundra, och det är skillnaden mellan ett dokument som växer linjärt med sitt sidantal och ett som inte gör det. Vattenstämplar, logotypstämplar, sidnumreringsmallar och sigill är alla samma typ av problem, och Form XObject är det rätta verktyget för vart och ett av dem.

Varför ett sparat objekt slår hundra ritningar

Besparingen är strukturell, inte kosmetisk. En PDF-sida renderas genom att köra dess innehållsström, en sekvens av ritoperatorer. När du ritar om en stämpel per sida lägger du till hela operatorsekvensen för den stämpeln till varje sidas ström, och byten dupliceras så många gånger som du har sidor. Ett Form XObject flyttar dessa operatorer till en enskild ström som lagras en gång i dokumentet. Referensen en enskild sida behåller är liten: den trycker en transformationsmatris, anropar XObjectet och återställer grafikläget. Sidantalet multiplicerar inte längre kostnaden för konstverket.

Detta är som mest betydelsefullt när stämpeln är tung. Ett vektorsigill med hundratals bansegment, eller en logotypsbitmapp, är dyrt att lagra. Lagrat en gång och refererat betalas den tunga delen en enda gång och overheadkostnaden per sida är några få byte för anropet. Det visuella resultatet på sidan är identiskt med en direkt omritning, vilket är poängen. Läsaren kan inte se skillnaden; filstorleken kan det i allra högsta grad.

Capturing a page into an XObject

PDFium bygger det återanvändbara objektet från en efterföljande sida. Källan är en sida i ett dokument du har öppet, en liten ensidig PDF som inte innehåller annat än ditt vattenstämpelkonstverk, eller en specifik sida i en större fil. CreateXObjectFromPage fångar den källsidans innehåll till ett återanvändbart handtag (handle) som ägs av måldokumentet, det du stämplar.

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

Signaturen är CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoden returnerar nil vid fel snarare än att kasta ett undantag, så den explicita kontrollen ovan är inte valfri. Det handtag som kommer tillbaka är ett TPdfXObject du äger, och de två livstidsbegränsningarna som är kopplade till det är den del av hela denna övning som brukar ställa till det för folk, så de får en egen sektion nedan.

Att placera stämpeln på en sida

Ett fångat XObject gör ingenting på egen hand. För att få det att visas sätter du in en kopia av det på dokumentets aktuella sida med InsertFormObjectFromXObject. Det anropet returnerar det underliggande sidobjektet, ett FPDF_PAGEOBJECT, och det returnerade handtaget är hur du positionerar placeringen. Utan en transformation landar stämpeln vid origo i källsidans egna koordinater, vilket sällan är där du vill ha den.

Eftersom InsertFormObjectFromXObject sätter in en kopia per anrop och lämnar tillbaka ett nytt sidobjekt varje gång, kan du måla samma XObject flera gånger på en sida vid olika transformationer, och det sparade innehållet räknas fortfarande en gång i filen. En hörnlogotyp och en svag helsidesvattenstämpel kan komma från samma fångade 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;

En ägandedetalj gör städningen säker. När det väl har satts in tillhör sidobjektet sidan, inte XObjectet. Att frigöra XObjectet senare ogiltigförklarar inte de placeringar du redan gjort. Det är det som låter ordningen skapa-placera-frigöra som beskrivs nedan att fungera.

Livstidsregeln för handtag som ställer till det för folk

Två begränsningar styr XObject-handtaget, och att ignorera någon av dem ger fel som ser ut att inte ha med saken att göra. För det första måste källdokumentet vara aktivt i det ögonblick du anropar CreateXObjectFromPage. Fångsten läser källsidans innehåll från det aktiva källdokumentet, så det dokumentet och dess sida måste vara öppna och giltiga när handtaget byggs. För det andra, och detta är det som överraskar folk, måste handtaget frigöras innan källsidan stängs, och i praktiken innan du stänger eller frigör källdokumentet det kom ifrån.

Orsaken är att XObjectet är en referens in i struktur som källdokumentet fortfarande äger. Det är inte en fristående, självständig kopia du kan bära med dig efter att källan är borta. Stäng källan först och handtaget lämnas pekande på innehåll som har rivits ner, så att frigöra det senare, eller någon annan användning av det, opererar på minne som inte längre är giltigt. Symptomet är det klassiska för ett hängande handtag: ett åtkomstfel (access violation) vid avstängning, eller periodisk korruption som flyttar runt beroende på allokeringsordning, med en stack som pekar på städkod snarare än på den rad som faktiskt orsakade problemet. Lösningen är ordningsföljd, inte defensiv kodning. Bygg XObjectet, sätt in det på varje sida som behöver det, frigör XObjectet och stäng först därefter källdokumentet. TPdfXObject-destruktorn släpper det underliggande PDFium-handtaget åt dig, så att frigöra omslaget vid rätt tidpunkt är hela ditt ansvar.

Matrisen och vad dess sex siffror betyder

Placering är en 2D-affin transformation, samma som PDF använder överallt för att positionera innehåll (ISO 32000-1, avsnitt 8.3.4). Det är sex siffror, skrivna a, b, c, d, e, f, och PDFium exponerar dem som posten FS_MATRIX. De mappar en punkt från objektets eget utrymme till sidutrymmet:

// 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 fylla i de sex värdena för hand, men att sätta ihop dem för hand är där rotation brukar gå fel, eftersom rotation blandar alla fyra av a, b, c, d tillsammans. Omslaget TPdfMatrix sätter ihop de vanliga operationerna åt dig och multiplicerar efter hand, så att Translate, Scale och Rotate kedjas i den ordning du anropar dem. En diagonal vattenstämpel är en rotation följd av en translation för att återcentrera den; en hörnlogotyp är en skalning följd av en translation. När matrisen är klar, lämna dess råa värde till FPDFPageObj_SetMatrix(PageObj, M.Handle), där M.Handle är den underliggande FS_MATRIX. Det lägre FPDFPageObj_Transform, som tar de sex värdena direkt som doubles, är tillgängligt när du hellre vill skicka siffror än bygga ett omslag.

Att stämpla varje sida i rätt ordning

Det fullständiga mönstret lägger samman delarna med den ordning som livstidsregeln kräver. Öppna båda dokumenten, fånga stämpeln en gång, vandra genom målsidorna genom att välja var och en i tur och ordning och sätta in samt positionera en kopia, frigör sedan XObjectet, spara sedan och låt källdokumentet stängas sist.

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-blocken gör det verkliga arbetet. Det inre finally-blocket frigör XObjectet innan kontrollen ens kan nå det yttre finally-blocket som frigör Stamp, så handtaget släpps alltid medan dess källa fortfarande lever, även om ett undantag skulle kastas mitt i loopen. Få det där nästlandet rätt och livstidsregeln tar hand om sig själv. (Använd den sidväljare som ditt bygge exponerar; loopkroppen är densamma oavsett.)

Stämpling är ett hörn av en större verktygslåda för att bygga och redigera sidinnehåll. Om din stämpel i sig är en bild snarare än en fångad sida, täcker att konverterar bilder till PDF-dokument med PDFium hur man får in den bitmappen i ett dokument först. Och när det du vill bära med dig bredvid den synliga stämpeln är en fil snarare än bläck på sidan, visar att arbeta med PDF-bilagor i Delphi sidan för inbäddade filer. Allt levereras med PDFium Component för Delphi och C++Builder, tillsammans med API:erna för rendering, redigering och dokument som beskrivs på andra ställen i den här bloggen.