Een watermerk of een logo op elke pagina van een document stempelen lijkt een klusje van een paar minuten, totdat u het resultaat opent in een bestandsgrootte-inspecteur. De voor de hand liggende aanpak is om de pagina's te doorlopen en op elke pagina dezelfde tekst- of afbeeldingsobjecten opnieuw op te bouwen. Dat werkt visueel, maar het is verspillend op een manier die zich opstapelt. Een diagonaal watermerk 'CONCEPT' dat rechtstreeks op een rapport van honderd pagina's wordt getekend, betekent honderd kopieën van dezelfde paden en tekstgegevens in de inhoudsstromen, en het opgeslagen bestand bevat ze allemaal.
Een Form XObject is de constructie die PDF biedt om precies dit te vermijden. Het verpakt een stuk herbruikbare inhoud, een hele pagina of een klein sjabloon, in een enkel benoemd object dat vele malen op vele posities kan worden getekend. De inhoud bevindt zich eenmalig in het bestand. Elke pagina die de stempel wil hebben, bevat een korte instructie die zegt: 'teken XObject N hier, met deze transformatie'. Een watermerk van honderd pagina's voegt dan één inhoudsobject toe aan het bestand in plaats van honderd, en dat is het verschil tussen een document dat lineair groeit met het aantal pagina's en een document dat dat niet doet. Watermerken, logostempels, paginanummersjablonen en zegels zijn allemaal dezelfde vorm van probleem, en het Form XObject is het juiste hulpmiddel voor elk van hen.
Waarom één opgeslagen object beter is dan honderd keer opnieuw tekenen
De besparing is structureel, niet cosmetisch. Een PDF-pagina wordt gerenderd door de inhoudsstroom uit te voeren, wat een reeks tekenbewerkingen (drawing operators) is. Wanneer u een stempel per pagina opnieuw tekent, voegt u de volledige bewerkingsreeks voor die stempel toe aan de stroom van elke pagina, en worden de bytes net zo vaak gedupliceerd als er pagina's zijn. Een Form XObject verplaatst die bewerkingen naar één enkele stroom die eenmalig in het document wordt opgeslagen. De verwijzing die een individuele pagina bijhoudt is klein: het pusht een transformatiematrix, roept het XObject aan en herstelt de status. Het aantal pagina's vermenigvuldigt de kosten van het artwork niet langer.
Dit is vooral belangrijk wanneer de stempel zwaar is. Een vectorzegel met honderden padsegmenten, of een logo-bitmap, is duur om op te slaan. Eenmaal opgeslagen en gerefereerd, worden de kosten voor het zware gedeelte eenmalig betaald en is de overhead per pagina slechts een paar bytes voor de aanroep. Het visuele resultaat op de pagina is identiek aan een directe hertekening, en dat is ook de bedoeling. De lezer merkt het verschil niet; de bestandsgrootte daarentegen wel.
Een pagina vastleggen in een XObject
De bron is een pagina in een document dat u open hebt staan, een kleine PDF van één pagina die niets anders bevat dan uw watermerkartwork, of een specifieke pagina van een groter bestand. CreateXObjectFromPage legt de inhoud van die bronpagina vast in een herbruikbare handle die eigendom is van het doeldocument, het document dat u stempelt.
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) ...
De signatuur is CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. De methode retourneert nil bij een fout in plaats van een uitzondering te genereren, dus de expliciete controle hierboven is niet optioneel. De handle die terugkomt is een TPdfXObject die u bezit, en the twee levensduurbeperkingen die eraan verbonden zijn, vormen het deel van deze hele oefening dat mensen vaak over het hoofd zien, dus krijgen ze hieronder hun eigen sectie.
De stempel op een pagina plaatsen
Een vastgelegd XObject doet uit zichzelf niets. Om het te laten verschijnen, voegt u er een kopie van in op de huidige pagina van het document met InsertFormObjectFromXObject. Die aanroep retourneert het onderliggende pagina-object, een FPDF_PAGEOBJECT, en de geretourneerde handle is de manier waarop u de plaatsing positioneert. Zonder een transformatie landt de stempel bij de oorsprong in de eigen coördinaten van de bronpagina, wat zelden is waar u het wilt.
Omdat InsertFormObjectFromXObject één kopie per aanroep invoegt en telkens een nieuw pagina-object teruggeeft, kunt u hetzelfde XObject meerdere keren op één pagina tekenen met verschillende transformaties, terwijl de opgeslagen inhoud nog steeds eenmalig in het bestand wordt geteld. Een hoeklogo en een vaag watermerk over de hele pagina kunnen uit hetzelfde vastgelegde object voortkomen.
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;
Eén detail over het eigendom maakt het opruimen veilig. Eenmaal ingevoegd, behoort het pagina-object toe aan de pagina, en niet aan het XObject. Het later vrijgeven van het XObject maakt de plaatsingen die u al hebt gedaan niet ongeldig. Dat is wat de hieronder beschreven volgorde 'maken-plaatsen-vrijgeven' mogelijk maakt.
De levensduurregel voor handles die mensen in de problemen brengt
Twee beperkingen bepalen de XObject-handle, en het negeren van een van beide leidt tot een fout die niets te maken lijkt te hebben met de oorzaak ervan. Ten eerste moet het brondocument actief zijn op het moment dat u CreateXObjectFromPage aanroept. De vastlegging leest de inhoud van de bronpagina uit het actieve brondocument, dus dat document en de bijbehorende pagina moeten open en geldig zijn wanneer de handle wordt gebouwd. Ten tweede, en dit is degene die mensen verrast, moet de handle worden vrijgegeven voordat de bronpagina wordt gesloten, en in de praktijk voordat u het brondocument waar deze vandaan kwam sluit of vrijgeeft.
De reden hiervoor is dat het XObject een referentie is naar een structuur die het brondocument nog steeds bezit. Het is geen losstaande, zelfstandige kopie die u kunt meenemen nadat de bron is verdwenen. Sluit de bron eerst en de handle blijft wijzen naar inhoud die is afgebroken, dus het later vrijgeven ervan, of elk ander gebruik ervan, werkt op geheugen dat niet langer geldig is. Het symptoom is de klassieke fout voor een zwevende handle: een toegangsfout (access violation) bij het afsluiten, of willekeurige corruptie die verschuift afhankelijk van de toewijzingsvolgorde, met een stack die wijst naar opruimcode in plaats van naar de regel die het probleem daadwerkelijk heeft veroorzaakt. De oplossing is volgorde, niet defensief programmeren. Bouw het XObject, voeg het in op elke pagina die het nodig heeft, geef het XObject vrij en sluit pas daarna het brondocument. De TPdfXObject-destructor geeft de onderliggende PDFium-handle voor u vrij, dus het op het juiste moment vrijgeven van de wrapper is volledig uw verantwoordelijkheid.
De matrix en wat de zes getallen betekenen
U kunt die zes waarden met de hand invullen, maar handmatig samenstellen is waar rotatie vaak misgaat, omdat rotatie alle vier de waarden a, b, c, d met elkaar mengt. De TPdfMatrix-wrapper stelt de algemene bewerkingen voor u samen en vermenigvuldigt ze doorlopend, zodat Translate, Scale en Rotate zich aaneenschakelen in de volgorde waarin u ze aanroept. Een diagonaal watermerk is een rotatie gevolgd door een translatie om het opnieuw te centreren; een hoeklogo is een schaling gevolgd door een translatie. Wanneer de matrix gereed is, geeft u de ruwe waarde door aan FPDFPageObj_SetMatrix(PageObj, M.Handle), waarbij M.Handle de onderliggende FS_MATRIX is. Het lagere niveau FPDFPageObj_Transform, dat de zes waarden rechtstreeks als doubles accepteert, is beschikbaar wanneer u liever getallen doorgeeft dan een wrapper bouwt.
// 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)
Elke pagina stempelen, in de juiste volgorde
Het volledige patroon brengt de stukken samen met de volgorde die de levensduurregel vereist. Open beide documenten, leg de stempel eenmaal vast, doorloop de doelpagina's waarbij u ze een voor een selecteert en een kopie invoegt plus positioneert, geef vervolgens het XObject vrij, sla op, en laat het brondocument als laatste sluiten.
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;
De vorm van de try-blokken doet het echte werk. De binnenste finally geeft het XObject vrij voordat de besturing ooit de buitenste finally kan bereiken die Stamp vrijgeeft, zodat de handle altijd wordt vrijgegeven terwijl de bron nog in leven is, zelfs als er halverwege de lus een uitzondering optreedt. Zorg dat die nesting correct is en de levensduurregel regelt zichzelf. (Gebruik de paginaselector die uw build biedt; de lusinhoud is in beide gevallen hetzelfde.)
Stempelen is een onderdeel van een grotere toolkit voor het bouwen en bewerken van pagina-inhoud. Als uw stempel zelf een afbeelding is in plaats van een vastgelegde pagina, behandelt afbeeldingen converteren naar PDF-documenten met PDFium hoe u die bitmap eerst in een document krijgt. En wanneer datgene wat u naast de zichtbare stempel wilt meesturen een bestand is in plaats van inkt op de pagina, laat werken met PDF-bijlagen in Delphi de kant van ingebedde bestanden zien. Dit alles wordt geleverd met het PDFium Component voor Delphi en C++Builder, naast de rendering-, bewerkings- en document-API's die elders op deze blog worden behandeld.