Å stemple et vannmerke eller en logo på hver side av et dokument virker som en femminutters jobb helt til du åpner resultatet i en filstørrelsesinspektør. Den opplagte tilnærmingen er å gå gjennom sidene og bygge de samme tekst- eller bildeobjektene på nytt på hver enkelt side. Det fungerer visuelt, men det er sløsing på en måte som forplanter seg. Et diagonalt "DRAFT"-vannmerke tegnet direkte på en hundresiders rapport er hundre kopier av samme sti- og tekstdata som sitter i innholdsstrømmene, og den lagrede filen bærer hver eneste en av dem.
En Form XObject er konstruksjonen PDF tilbyr for å unngå akkurat dette. Den pakker inn en bit gjenbrukbart innhold, en hel side eller en liten mal, 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 har en kort instruksjon som sier "mal XObject N her, med denne transformasjonen." Et hundresiders vannmerke legger da til ett innholdsobjekt i filen i stedet for hundre, og det er forskjellen mellom et dokument som vokser lineært med sideantallet sitt og et som ikke gjør det. Vannmerker, logostempler, sidenummermaler og segl er alle samme type problem, og Form XObject er det rette verktøyet for hver enkelt av dem.
Hvorfor ett lagret objekt slår hundre ometegninger
Besparelsen er strukturell, ikke kosmetisk. En PDF-side gjengis ved å kjøre sin innholdsstrøm, en sekvens av tegneoperatorer. Når du tegner et stempel på nytt per side, legger du til hele operatorsekvensen for det stempelet på hver sides strøm, og bytene dupliseres like mange ganger som du har sider. En Form XObject flytter disse operatorene inn i én strøm som lagres én gang i dokumentet. Referansen en enkelt side beholder er liten: den skyver en transformasjonsmatrise, kaller XObjectet og gjenoppretter status. Sideantallet multipliserer ikke lenger kostnaden for grafikken.
Dette betyr mest når stempelet er tungt. Et vektorsegl med hundrevis av stisegmenter, eller en logopunktgrafikk, er kostbart å lagre. Lagret én gang og referert, betales den tunge delen én enkelt gang, og overheaden per side er noen få byte med oppkalling. Det visuelle resultatet på siden er identisk med en direkte ometegning, som er poenget. Leseren kan ikke se forskjellen; filstørrelsen kan det i aller høyeste grad.
Fange en side inn i en XObject
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) ...PDFium bygger det gjenbrukbare objektet fra en eksisterende side. Kilden er en side i et dokument du har åpent, en liten ensides PDF som ikke inneholder annet enn vannmerkegrafikken din, eller en bestemt side i en større fil. CreateXObjectFromPage fanger kildesidens innhold inn i et gjenbrukbart håndtak som tilhører destinasjonsdokumentet, det du stempler.
Plassere stempelet på en side
En fanget XObject gjør ingenting alene. For å få den til å vises, setter du inn en kopi av den på dokumentets gjeldende side 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 i origo i kildesidens egne koordinater, som sjelden er der du vil ha det.
Fordi InsertFormObjectFromXObject setter inn én kopi per kall og gir tilbake et friskt sideobjekt hver gang, kan du male samme XObject flere ganger på én side med forskjellige transformasjoner, og det lagrede innholdet telles fortsatt én gang i filen. En hjørnelogo og et svakt fullsides vannmerke kan komme fra det samme fangede objektet.
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;Regelen for håndtakets levetid som biter folk
To begrensninger styrer XObject-håndtaket, og å ignorere en av dem produserer en feil som virker urelatert til årsaken. For det første må kildedokumentet være aktivt i det øyeblikket du kaller CreateXObjectFromPage. Fangsten leser kildesidens innhold fra det levende kildedokumentet, så det dokumentet og siden må være åpne og gyldige når håndtaket bygges. For det andre, og dette er den som overrasker folk, må håndtaket frigjøres 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 struktur som kildedokumentet fortsatt eier. Det er ikke en frittstående, selvstendig kopi du kan bære med deg etter at kilden er borte. Lukker du kilden først, blir håndtaket stående pekende på innhold som er revet ned, så å frigjøre det senere, eller all annen bruk av det, opererer på minne som ikke lenger er gyldig. Symptomet er det klassiske for et dinglende håndtak: en tilgangsfeil (access violation) ved avslutning, eller periodisk korrupsjon som flytter seg rundt avhengig av allokeringsrekkefølge, med en stabel (stack) som peker på oppryddingskode i stedet for på linjen som faktisk forårsaket problemet. Løsningen er rekkefølge, ikke defensiv koding. Bygg XObjectet, sett det inn på hver side som trenger det, frigjør XObjectet, og først da lukker du kildedokumentet. TPdfXObject-destruktøren frigjør det underliggende PDFium-håndtaket for deg, så å frigjøre innpakningen til rett tid er hele ansvaret ditt.
Matrisen, og hva dens seks tall betyr
Plassering er en 2D-affin transformasjon, 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. De avbilder et punkt fra objektets eget rom til siderommet:
// 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 å sette dem sammen for hånd er der rotasjon slår feil, fordi rotasjon blander alle fire av a, b, c, d sammen. TPdfMatrix-innpakningen setter sammen de vanlige operasjonene for deg og etter-multipliserer etter hvert, slik at Translate, Scale og Rotate kobles sammen i den rekkefølgen du kaller dem. Et diagonalt vannmerke er en rotasjon fulgt av en translasjon for å resentrere det; en hjørnelogo er en skalering fulgt av en translasjon. Når matrisen er klar, overleverer du dens råverdi til FPDFPageObj_SetMatrix(PageObj, M.Handle), der M.Handle is den underliggende FS_MATRIX. Den lavere FPDFPageObj_Transform, som tar de seks verdiene direkte som double, er tilgjengelig når du heller vil sende tall enn å bygge en innpakning.
Stemple hver side, i riktig rekkefølge
Det fulle mønsteret setter sammen bitene med rekkefølgen som levetidsregelen krever. Åpne begge dokumenter, fang stempelet én gang, gå gjennom destinasjonssidene og velg hver enkelt etter tur og sett inn pluss posisjoner en kopi, frigjør deretter XObjectet, lagre deretter, og la kildedokumentet lukkes til slutt.
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;Formene på try-blokkene gjør det virkelige arbeidet. Den indre finally frigjør XObjectet før kontrollen kan nå den ytre finally som frigjør Stamp, slik at håndtaket alltid slippes mens kilden er i live, selv om et unntak utløses midt i løkken. Få den nøstingen riktig, så tar levetidsregelen vare på seg selv. (Bruk den gjeldende sidevelgeren som bygget ditt eksponerer; løkkekroppen er den samme uansett.)
Stamping is one corner of a larger toolkit for building and editing page content. If your stamp is itself an image rather than a captured page, converting images to PDF documents with PDFium covers getting that bitmap into a document first. And when the thing you want to carry alongside the visible stamp is a file rather than ink on the page, working with PDF attachments in Delphi shows the embedded-file side. All of it ships with the PDFium Component for Delphi and C++Builder, alongside the rendering, editing, and document APIs covered elsewhere on this blog.