At stemple et vandmærke eller et logo på hver side i et dokument ligner et fem-minutters job, indtil du åbner resultatet i en filstørrelses-inspektør (file-size inspector). Den åbenlyse tilgang er at gennemgå siderne og på hver af dem bygge de samme tekst- eller billedobjekter igen. Det fungerer visuelt, og det er spild (wasteful) på en måde, der forstærkes (compounds). Et diagonalt "UDKAST" (DRAFT) vandmærke tegnet direkte på en hundredesiders rapport er hundrede kopier af de samme sti- og tekstdata (path and text data), der sidder i indholdsstrømmene (the content streams), og den gemte fil bærer hver og en af dem
Et Form XObject er den konstruktion, PDF leverer for at undgå præcis dette. Den indpakker et stykke genanvendeligt indhold, en hel side eller en lille skabelon (template), i et enkelt navngivet objekt, der kan males (painted) mange gange på mange positioner. Indholdet lever i filen én gang. Hver side, der ønsker stemplet, rummer en kort instruktion, der siger "mal XObject N her, med denne transformation" (paint XObject N here, with this transform). Et hundrede-siders vandmærke tilføjer derefter ét indholdsobjekt til filen snarere end hundrede, og det er forskellen mellem et dokument, der vokser lineært med sit sideantal, og et, der ikke gør. Vandmærker, logo-stempler, sidetal-skabeloner og segl (seals) er alle den samme form for problem, og Form XObject er det rigtige værktøj til hver og en af dem
Hvorfor ét gemt objekt slår hundrede gentegninger (redraws)
Besparelsen er strukturel, ikke kosmetisk. En PDF-side rendereres ved at udføre dens indholdsstrøm, en sekvens af tegningsoperatører (drawing operators). Når du gentegner et stempel pr. side, tilføjer (appending) du den fulde operatørsekvens for det stempel til hver sides strøm, og bytene (the bytes) kopieres lige så mange gange, som du har sider. Et Form XObject flytter disse operatører over i én strøm gemt én gang i dokumentet. Referencen en individuel side beholder er lille: den skubber (pushes) en transformationsmatrix (transformation matrix), påkalder (invokes) XObject'et og gendanner (restores) tilstanden. Sideantallet multiplicerer ikke længere omkostningerne ved kunstværket (the artwork)
Dette betyder mest, når stemplet er tungt. Et vektorsegl (vector seal) med hundredvis af stisegmenter, eller en logo-bitmap, er dyrt at gemme. Gemt én gang og refereret (referenced), betales den tunge del for en enkelt gang, og pr.-side overheadet (per-page overhead) er nogle få bytes af påkaldelse. Det visuelle resultat på siden er identisk med en direkte gentegning, hvilket er pointen. Læseren kan ikke se forskel; filstørrelsen kan i høj grad
Indfangning (Capturing) af en side i et XObject
PDFium bygger det genanvendelige objekt fra en eksisterende side. Kilden er en side i et eller andet dokument, du har åbent, en lille én-sides PDF, der ikke indeholder andet end dit vandmærkearbejde (watermark artwork), eller en bestemt side af en større fil. CreateXObjectFromPage indfanger denne kildesides indhold i et genanvendeligt handle, der tilhører destinationsdokumentet, det du stempler på
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 en undtagelse (raises), hvis kildedokumentet ikke er Active, og den returnerer nil snarere end at kaste (raising), når PDFium ikke kan bygge objektet, så det eksplicitte tjek (check) ovenfor er ikke valgfrit. Det handle, der kommer tilbage, er et TPdfXObject du ejer, og de to livstidsbegrænsninger (lifetime constraints) knyttet til det er den del af hele denne øvelse, der overrasker folk (catches people out), så de får deres egen sektion nedenfor
Placering af stemplet på en side
Et indfanget XObject gør intet af sig selv. For at få det til at fremkomme, indsætter du en kopi af det på dokumentets aktuelle side, den der er valgt af den 1-baserede PageNumber egenskab, med InsertFormObjectFromXObject. Det kald returnerer det underliggende sideobjekt (page object), en FPDF_PAGEOBJECT, og det returnerede handle er, hvordan du positionerer placeringen. Uden en transformation (transform) lander stemplet ved origo (the origin) i kildesidens egne koordinater, hvilket sjældent er, hvor du ønsker det
Fordi InsertFormObjectFromXObject indsætter én kopi pr. kald og giver et friskt sideobjekt tilbage hver gang, kan du male det samme XObject adskillige gange på én side med forskellige transformationer, og det lagrede indhold tælles stadig én gang i filen. Et hjørnelogo og et svagt fuldsides vandmærke kan komme fra det samme indfangede objekt
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 husholdningsdetaljer (housekeeping details) gør dette sikkert. For det første, når det er indsat, tilhører sideobjektet siden, ikke XObject'et. At frigøre XObject'et senere ugyldiggør ikke de placeringer, du allerede har lavet. Det er det, der lader den skab-placer-frigør (create-place-free) rækkefølge beskrevet nedenfor virke. For det andet ændrer indsættelse og positionering kun sidens objektliste i hukommelsen; UpdatePage er det, der serialiserer (serialises) den liste tilbage i sidens indholdsstrøm, så en side, du redigerer uden at kalde den, gemmes, som om stemplet aldrig blev placeret
Handle-livstidsreglen, der bider folk
To begrænsninger (constraints) styrer XObject-handle'et, og at ignorere en af dem producerer en fejl, der ser urelateret ud til dens årsag. For det første skal kildedokumentet være aktivt på det tidspunkt, du kalder CreateXObjectFromPage. Indfangningen læser kildesidens indhold fra det levende (live) kildedokument, så det dokument og dets side skal være åbne og gyldige, når handle'et bygges. For det andet, og det er den, der overrasker folk, skal handle'et frigøres, før kildesiden lukkes, og i praksis før du lukker eller frigør det kildedokument, det kom fra
Grunden er, at XObject'et er en reference ind i en struktur, som kildedokumentet stadig ejer. Det er ikke en løsrevet (detached), selvstændig (self-contained) kopi, du kan bære rundt på, efter kilden er væk. Luk kilden først, og handle'et efterlades pegende på indhold, der er blevet revet ned (torn down), så at frigøre 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 (dangling handle): en adgangskrænkelse (access violation) ved nedlukning (shutdown), eller intermitterende korruption (intermittent corruption), der flytter sig rundt afhængigt af allokeringsrækkefølgen (allocation order), med en stak (stack), der peger på oprydningskode i stedet for på den linje, der faktisk forårsagede problemet. Løsningen er rækkefølge (ordering), 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å at frigøre indpakningen (the wrapper) på det rigtige tidspunkt er hele dit ansvar
Matrixen, og hvad dens seks tal betyder
Placering er en 2D affin transformation (affine transform), den samme som PDF bruger overalt til positionering af indhold (ISO 32000-1, afsnit 8.3.4). Det er seks tal, skrevet a, b, c, d, e, f, og PDFium udstiller dem som FS_MATRIX posten (record). De afbilder (map) et punkt fra objektets eget rum (space) til siderum (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 udfylde de seks værdier i hånden, men at sammensætte (composing) dem i hånden er dér, hvor rotation går galt, fordi rotation blander alle fire af a, b, c, d sammen. TPdfMatrix indpakningen (the wrapper), fra FPdfMatrix enheden (unit), sammensætter de almindelige operationer for dig og post-multiplicerer undervejs, så Translate, Scale og Rotate kæder sig i den rækkefølge, du kalder dem. Et diagonalt vandmærke er en rotation efterfulgt af en forskydning (translate) for at gencentrere det; et hjørnelogo er en skalering (scale) efterfulgt af en forskydning. Når matrixen er klar, kopier dens rå værdi, Handle egenskaben af typen FS_MATRIX, ind i en lokal variabel og send den til FPDFPageObj_SetMatrix; importen (the import) deklarerer matrixen som en var parameter, så en egenskab (property) kan ikke gives direkte til den, og dens resultat er 0 ved fejl. Det lavere niveau FPDFPageObj_Transform, som tager de seks værdier direkte som doubles (doubles), er tilgængeligt, når du hellere vil sende tal end at bygge en indpakning
Stempling af hver side, i den rigtige rækkefølge
Det fulde mønster (pattern) sætter brikkerne sammen med den rækkefølge, livstidsreglen (the lifetime rule) kræver. Åbn begge dokumenter, indfang (capture) stemplet én gang, gennemgå (walk) destinationssiderne ved at indstille den 1-baserede PageNumber efter tur og indsætte samt positionere en kopi, overgive (committing) hver side med UpdatePage, frigør (free) derefter XObject'et, gem derefter med SaveAs, og lad kildedokumentet lukke til sidst
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 udfører det virkelige arbejde. Den indre finally frigør XObject'et, før kontrollen (control) nogensinde kan nå den ydre finally, der frigør Stamp, så handle'et bliver altid frigivet, mens dets kilde stadig er i live, selv hvis en undtagelse udløses (fires) midt i løkken. Få den indlejring (nesting) rigtig, og livstidsreglen tager sig af sig selv
Stempling er ét hjørne af et større værktøjssæt (toolkit) til at bygge og redigere sideindhold. Hvis dit stempel i sig selv er et billede snarere end en indfanget side, dækker konvertering af billeder til PDF-dokumenter med PDFium at få den bitmap ind i et dokument først. Og når det, du ønsker at bære ved siden af det synlige stempel, er en fil snarere end blæk på siden, viser arbejde med PDF-vedhæftninger i Delphi siden med indlejrede filer (the embedded-file side). Det hele leveres med PDFium-komponenten til Delphi og C++Builder, sammen med renderings-, redigerings- og dokument-API'erne dækket andre steder på denne blog