Teknisk artikel

Å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 fem-minutersjobb tills du öppnar resultatet i en filstorleksinspektör. Det uppenbara tillvägagångssättet är att gå igenom sidorna och, på var och en, bygga samma text eller bildobjekt igen. Det fungerar visuellt, och det är slösaktigt på ett sätt som förvärras. En diagonal "UTKAST"-vattenstämpel ritad direkt på en hundrasidig rapport är hundra kopior av samma sök- och textdata som sitter i innehållsströmmarna, och den sparade filen bär var och en av dem

Ett Form XObject är den konstruktion PDF tillhandahåller för att undvika exakt detta. Det slår in en bit återanvändbart innehåll, en hel sida eller en liten mall, till ett enda namngivet objekt som kan målas många gånger på många positioner. Innehållet lever i filen en gång. Varje sida som vill ha stämpeln innehåller en kort instruktion som säger "måla XObject N här, med denna transformation." En hundrasidig vattenstämpel lägger då till ett innehållsobjekt i filen istället för 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, sidnummermallar och sigill är alla samma form av problem, och Form XObject är rätt verktyg för vart och ett av dem

Diagram som kontrasterar att rita vattenstämpeloperatorer på varje PDF-sida, mot att lagra dem en gång i ett Form XObject med PDFium
Att rita om stämpeln på varje sida duplicerar dess byte över varje innehållsström, medan en Form XObject lagrar konstverket en gång och låter varje sida referera det

Varför ett lagrat objekt slår hundra omritningar

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

Detta spelar störst roll när stämpeln är tung. Ett vektorsigill med hundratals segment, eller en logotypbitmapp, är dyrt att lagra. Lagrad en gång och refererad, är den tunga delen betald en enda gång och per-sida-overheaden är ett par byte av anrop. Det visuella resultatet på sidan är identiskt med en direkt omritning, vilket är poängen. Läsaren kan inte se någon skillnad; filstorleken kan mycket väl göra det

Att fånga en sida in i ett XObject

PDFium bygger det återanvändbara objektet från en befintlig sida. Källan är en sida i något dokument du har öppet, en liten ensidig PDF som inte innehåller något annat än din vattenstämpelgrafik, eller en specifik sida i en större fil. CreateXObjectFromPage fångar källsidans innehåll in i ett återanvändbart handtag som tillhör destinationsdokumentet, det du stämplar

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';   // en sida grafik
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // Fånga sida 0 av stämpeldokumentet i ett återanvändbart handtag som
    // ägs av Dest. Källan måste vara Active; indexet är nollbaserat.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... placera det, frigör det sedan före att stänga Stamp (se nedan) ...

Signaturen är CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoden kastar ett fel om källdokumentet inte är Active, och den returnerar nil snarare än att kasta ett fel när PDFium inte kan bygga objektet, så den explicita kontrollen ovan är inte valfri. Handtaget som kommer tillbaka är ett TPdfXObject som du äger, och de två livslängdsbegränsningarna kopplade till det är den del av hela denna övning som överraskar folk, så de får ett eget avsnitt nedan

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, den som valts av den 1-baserade egenskapen PageNumber, 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 i 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 lagrade innehållet räknas fortfarande en gång i filen. En hörnlogotyp och en svag helsidesvattenstämpel kan komma från samma infångade objekt

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
begin
  // Den aktuella sidan av Dest tar emot en kopia av XObject.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Positionera det: flytta 200 enheter höger, 500 upp, vid 70% skala.
  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;   // checka in denna sidas redigeringar i dess innehållsström
  // om inte Dest.SaveAs(...) så ... när varje sida är klar.
end;

Två städningsdetaljer gör detta säkert. För det första, när det väl är insatt tillhör sidobjektet sidan, inte XObject. Att frigöra XObject senare ogiltigförklarar inte de placeringar du redan gjort. Det är det som låter skapa-placera-frigör-ordningen som beskrivs nedan fungera. För det andra ändrar införande och positionering bara sidans objektlista i minnet; UpdatePage är det som serialiserar den listan tillbaka till sidans innehållsström, så en sida du redigerar utan att anropa den sparas som om stämpeln aldrig hade placerats

Regeln för handtags livslängd som biter folk

Två begränsningar styr XObject-handtaget, och att ignorera någon av dem leder till ett misslyckande som ser orelaterat ut till sin orsak. För det första måste källdokumentet vara aktivt i det ögonblick du anropar CreateXObjectFromPage. Infångningen läser källsidans innehåll från det levande källdokumentet, så det dokumentet och dess sida måste vara öppna och giltiga när handtaget byggs. För det andra, och det är den här 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

Anledningen är att XObject är en referens in i strukturen som källdokumentet fortfarande äger. Det är inte en frikopplad, fristående kopia som du kan bära runt på efter att källan är borta. Stäng källan först och handtaget lämnas pekandes på innehåll som har rivits ner, så att frigöra det senare, eller all annan användning av det, opererar på minne som inte längre är giltigt. Symtomet är det klassiska för ett hängande handtag: en åtkomstöverträdelse vid avslutning, eller intermittent korruption som flyttar runt beroende på allokeringsordning, med en stack som pekar på städningskod snarare än på raden som faktiskt orsakade problemet. Lösningen är ordning, inte defensiv kodning. Bygg XObject, sätt in det på varje sida som behöver det, frigör XObject, och först därefter stänger du källdokumentet. Destruktorn TPdfXObject släpper det underliggande PDFium-handtaget åt dig, så att frigöra omslaget vid rätt tidpunkt är hela ditt ansvar

Ordnat livscykeldiagram för PDFium sidstämplar som visar infångning, placering, frigörande av TPdfXObject-handtaget och stängning av stämpeldokumentet sist
Fånga stämpeln en gång, placera den på varje sida, frigör XObject:en medan stämpeldokumentet fortfarande är öppet, spara och stäng sedan källan sist

Matrisen, och vad dess sex siffror betyder

Placering är en 2D affin transformation, densamma som PDF använder överallt för positionering av 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 egen rymd till sidans rymd:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horisontell och vertikal skala
// b, c : the shear / rotation terms
// e, f : translation (var origo hamnar på sidan)

Du kan fylla dessa sex värden för hand, men att komponera dem för hand är där rotation går fel, eftersom rotation blandar alla fyra av a, b, c, d tillsammans. Omslaget TPdfMatrix, från FPdfMatrix-enheten, komponerar de vanliga operationerna åt dig och efter-multiplicerar vartefter den går, så Translate, Scale och Rotate kedjas i den ordning du anropar dem. En diagonal vattenstämpel är en rotering följt av en förskjutning för att centrera den igen; en hörnlogotyp är en skalning följt av en förskjutning. När matrisen är klar, kopierar du dess råa värde, egenskapen Handle av typen FS_MATRIX, in i en lokal variabel och skickar den till FPDFPageObj_SetMatrix; importen deklarerar matrisen som en var-parameter, så en egenskap kan inte överlämnas till den direkt, och dess resultat är 0 vid fel. Den lägre nivån FPDFPageObj_Transform, som tar de sex värdena direkt som flyttal, är tillgänglig när du hellre skickar siffror än bygger ett omslag

Stämpla varje sida, i rätt ordning

Hela mönstret sätter ihop bitarna med den ordning livslängdsregeln kräver. Öppna båda dokumenten, fånga stämpeln en gång, gå igenom destinationssidorna genom att ställa in det 1-baserade PageNumber i tur och ordning och sätta in plus positionera en kopia, checka in varje sida med UpdatePage, frigör sedan XObject, spara sedan med SaveAs, 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;
  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. Fånga grafiken en gång. Stamp är Active här.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Placera en kopia på varje sida av Dest. PageNumber är 1-baserat.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // gör sida I aktuell
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // diagonal vattenstämpel
          M.Translate(150, 100);             // justera in i position
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // checka in denna sidas redigeringar
      end;
    finally
      XObject.Free;                          // 3. frigör INNAN Stamp stängs
    end;

    // 4. Skriv resultatet medan Dest fortfarande är öppen.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // källan stängs sist
    Dest.Free;
  end;
end;

Formen på try-blocken gör det riktiga arbetet. Det inre finally frigör XObject innan kontrollen någonsin kan nå det yttre finally som frigör Stamp, så handtaget släpps alltid medan dess källa fortfarande lever, även om ett undantag kastas mitt i loopen. Få till den nästlingen rätt och livslängdsregeln tar hand om sig själv

FS_MATRIX-anatomi som visar de sex affina koefficienterna PDFium använder för att skala, rotera och översätta en stämpelad Form XObject på sidan
Sex tal mappar stämpelkoordinater in i sidrymden, och TPdfMatrix komponerar Scale, Rotate och Translate i anropsordning för att placera en diagonal vattenstämpel

Att stämpla ä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 infångad sida, täcker konvertering av bilder till PDF-dokument med PDFium att få in den bitmappen i ett dokument först. Och när det du vill bära med dig jämte den synliga stämpeln är en fil snarare än bläck på sidan, visar att arbeta med PDF-bilagor i Delphi den inbäddade filsidan. Allt detta levereras med PDFium Component för Delphi och C++Builder, bredvid renderaren, redigeraren och de dokument-API:er som beskrivs på annat håll i den här bloggen