Teknisk artikkel

Gjenbrukbare sidestempler via Form XObjects med PDFium

Å stemple et vannmerke eller en logo på hver side i et dokument ser ut som en fem minutters jobb helt til du åpner resultatet i et filstørrelsesinspektør. Den åpenbare tilnærmingen er å gå gjennom sidene og, på hver av dem, bygge de samme tekst- eller bildeobjektene på nytt. Det fungerer visuelt, og det er sløsing på en måte som forsterkes. Et diagonalt "DRAFT"-vannmerke tegnet direkte på en hundresiders rapport er hundre kopier av de samme sti- og tekstdataene som sitter i innholdsstrømmene (content streams), og den lagrede filen bærer hver eneste av dem

En Form XObject er konstruksjonen PDF gir for å unngå akkurat dette. Den pakker inn en bit gjenbrukbart innhold, en hel side eller en liten mal, inn i et enkelt navngitt objekt som kan males mange ganger på mange posisjoner. Innholdet lever i filen én gang. Hver side som vil ha stempelet, holder en kort instruksjon som sier "mal XObject N her, med denne transformasjonen." Et vannmerke over hundre sider legger deretter ett innholdsobjekt til filen snarere enn hundre, og det er forskjellen mellom et dokument som vokser lineært med sidetallet og et som ikke gjør det. Vannmerker, logostempler, sidetallsmaler og segl er alle samme form for problem, og Form XObject er det rette verktøyet for hver av dem

Hvorfor ett lagret objekt slår hundre gentegninger

Besparelsen er strukturell, ikke kosmetisk. En PDF-side gjengis ved å utføre dens innholdsstrøm (content stream), en sekvens av tegneoperatører. Når du tegner om (redraw) et stempel per side, legger du den fulle operatørsekvensen for det stempelet til hver sides strøm, og bytene dupliseres like mange ganger som du har sider. En Form XObject flytter disse operatørene inn i én strøm som lagres én gang i dokumentet. Referansen en individuell side beholder er liten: den skyver (pushes) en transformasjonsmatrise (transformation matrix), påkaller (invokes) XObjectet, og gjenoppretter tilstand (restores state). Sidetallet multipliserer ikke lenger kostnaden av kunstverket

Dette betyr mest når stempelet er tungt. Et vektorsegl med hundrevis av stisegmenter, eller en logobitekart, er dyrt å lagre. Lagret én gang og referert til, er den tunge delen betalt for én enkelt gang, og kostnaden per side er noen få byte for påkallelsen. Det visuelle resultatet på siden er identisk med en direkte gentegning (redraw), noe som er poenget. Leseren kan ikke se forskjellen; det kan filstørrelsen i høyeste grad

Fange en side inn i en XObject

PDFium bygger det gjenbrukbare objektet fra en eksisterende side. Kilden er en side i et dokument du har åpent, en liten en-sides PDF som inneholder intet annet enn vannmerkekunstverket ditt, eller en bestemt side i en større fil. CreateXObjectFromPage fanger innholdet på den kildesiden inn i et gjenbrukbart håndtak (handle) som tilhører måldokumentet, det du stempler

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

Signaturen er CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoden kaster et unntak (raises) hvis kildedokumentet ikke er Active, og den returnerer nil fremfor å heve et unntak når PDFium ikke kan bygge objektet, så den eksplisitte sjekken over er ikke valgfri. Håndtaket (handle) som kommer tilbake er et TPdfXObject du eier, og de to levetidsbegrensningene knyttet til det er den delen av denne hele øvelsen som feller (catches out) folk, så de får sin egen seksjon nedenfor

Plassering av stempelet på en side

En fanget XObject gjør ingenting på egen hånd. For å få det til å vises, setter du inn en kopi av det på dokumentets gjeldende side, den som er valgt av den 1-baserte egenskapen PageNumber, med InsertFormObjectFromXObject. Det kallet returnerer det underliggende sideobjektet, en FPDF_PAGEOBJECT, og det returnerte håndtaket er hvordan du posisjonerer plasseringen. Uten en transformasjon lander stempelet ved origo (origin) i kildesidens egne koordinater, noe som sjelden er der du vil ha det

Siden InsertFormObjectFromXObject setter inn én kopi per kall og gir tilbake et ferskt sideobjekt hver gang, kan du male den samme XObject flere ganger på én side ved forskjellige transformasjoner, og det lagrede innholdet telles fortsatt én gang i filen. En hjørnelogo og et svakt vannmerke for hele siden kan komme fra det samme fangede objektet

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;

To ryddedetaljer (housekeeping details) gjør dette trygt. Først og fremst, når det er satt inn, tilhører sideobjektet siden, ikke XObject. Å frigjøre (freeing) XObjectet senere ugyldiggjør ikke plasseringene du allerede har gjort. Det er det som lar opprett-plasser-frigjør-rekkefølgen beskrevet nedenfor fungere. For det andre endrer det å sette inn og posisjonere bare sidens objektliste i minnet; UpdatePage er det som serialiserer (serialises) den listen tilbake til sidens innholdsstrøm, så en side du redigerer uten å kalle den lagres som om stempelet aldri ble plassert

Håndtakets levetidsregel som biter folk

To begrensninger styrer XObject-håndtaket, og å ignorere en av dem produserer en feil som ser urelatert ut til årsaken. Først må kildedokumentet være aktivt (active) i øyeblikket du kaller CreateXObjectFromPage. Fangsten (capture) leser kildesidens innhold fra det levende kildedokumentet, så det dokumentet og dets side må være åpne og gyldige når håndtaket bygges. For det andre, og det er dette som overrasker folk, må håndtaket frigjøres (freed) før kildesiden lukkes, og i praksis før du lukker eller frigjør kildedokumentet det kom fra

Årsaken er at XObjectet er en referanse inn i strukturer som kildedokumentet fortsatt eier. Det er ikke en løsrevet, selvoppholdende kopi du kan bære rundt etter at kilden er borte. Lukk kilden først og håndtaket etterlates pekende på innhold som har blitt revet ned, så å frigjøre det senere, eller enhver annen bruk av det, opererer på minne som ikke lenger er gyldig. Symptomet er det klassiske for et dinglende (dangling) håndtak: et tilgangsbrudd (access violation) ved avslutning, eller intermitterende korrupsjon som flytter seg rundt avhengig av tildelingsrekkefølgen (allocation order), med en stabel (stack) som peker på oppryddingskode heller enn på linjen som faktisk forårsaket problemet. Reparasjonen er rekkefølge, ikke defensiv koding. Bygg XObjectet, sett det inn på hver side som trenger det, frigjør XObjectet, og først deretter lukker du kildedokumentet. TPdfXObject-destruktøren frigir det underliggende PDFium-håndtaket for deg, så å frigjøre innpakningen (wrapper) til rett tid er hele ditt ansvar

Matrisen, og hva dens seks tall betyr

Plassering er en 2D-affin transformasjon (affine transform), den samme som PDF bruker overalt for å posisjonere innhold (ISO 32000-1, seksjon 8.3.4). Det er seks tall, skrevet a, b, c, d, e, f, og PDFium eksponerer dem som FS_MATRIX-posten (record). De kartlegger et punkt fra objektets eget rom (space) til siderom (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)

Du kan fylle disse seks verdiene for hånd, men å komponere dem for hånd er der rotasjon går galt, fordi rotasjon blander alle fire a, b, c, d sammen. TPdfMatrix-innpakningen (wrapper), fra FPdfMatrix-enheten, komponerer de vanlige operasjonene for deg og post-multipliserer ettersom det går, så Translate, Scale og Rotate kjedes (chain) i den rekkefølgen du kaller dem. Et diagonalt vannmerke er en rotasjon (rotate) fulgt av en oversettelse (translate) for å sentrere det på nytt; en hjørnelogo er en skalering (scale) fulgt av en oversettelse. Når matrisen er klar, kopier dens rå verdi, Handle-egenskapen av type FS_MATRIX, inn i en lokal variabel og send den til FPDFPageObj_SetMatrix; importen deklarerer matrisen som en var-parameter, så en egenskap kan ikke gis til den direkte, og resultatet er 0 ved feil. Den mer lavnivå-baserte FPDFPageObj_Transform, som tar de seks verdiene direkte som dobbelpresisjonstall (doubles), er tilgjengelig når du heller vil sende tall enn å bygge en innpakning

Stempling av hver side, i riktig rekkefølge

Det fulle mønsteret setter delene sammen med den rekkefølgen levetidsregelen (lifetime rule) krever. Åpne begge dokumentene, fang stempelet én gang, gå gjennom målsidene ved å sette den 1-baserte PageNumber i tur og orden og sette inn pluss posisjonere en kopi, bekrefte (commit) hver side med UpdatePage, frigjør deretter XObjectet, lagre deretter med SaveAs, og la kildedokumentet lukkes 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. 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;

Formen på try-blokkene gjør det virkelige arbeidet. Den indre finally frigjør XObjectet før kontrollen noen gang kan nå den ytre finally som frigjør Stamp, så håndtaket frigis alltid mens kilden fortsatt lever, selv om et unntak fyrer av midt i løkken. Få den nøstingen (nesting) riktig, og levetidsregelen tar vare på seg selv

Stempling er ett hjørne av et større verktøysett for å bygge og redigere sideinnhold. Hvis stempelet ditt i seg selv er et bilde snarere enn en fanget side, dekker konvertering av bilder til PDF-dokumenter med PDFium å få det bitkartet inn i et dokument først. Og når det du vil ha med deg sammen med det synlige stempelet er en fil i stedet for blekk på siden, viser arbeid med PDF-vedlegg i Delphi den innebygde-fil-siden (embedded-file side). Alt dette følger med PDFium-komponenten for Delphi og C++Builder, sammen med gjengivelses-, redigerings- og dokument-API-ene dekket andre steder på denne bloggen