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
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'; // 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 ä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
// 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;
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
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 : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
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. 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-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
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