PDFlibPas (losLab PDF Library) vult PDF-bloktemplates vanuit Delphi: een blok is een benoemde rechthoek op een templatepagina, getypeerd als tekst, image of PDF, en FillTextBlock, FillImageBlock en FillPDFBlock tekenen variabele data erin. Blokken staan als PDFlibBlock:Text-, PDFlibBlock:Image- en PDFlibBlock:PDF-paginaannotaties, of in een page-piece dictionary onder /PieceInfo
De use case is het saaie, hoogvolume soort. Een ontwerper zet een certificaat, een eventticket of een aanmaning één keer op, en het programma stempelt een naam, een stoelnummer, een handtekeningimage of een voorgerenderde barcodepagina in vaste vakken voor tienduizend records. Formuliervelden kunnen een deel daarvan, maar een AcroForm-veld is een interactief object met zijn eigen appearance stream en zijn eigen regels, en u vlakt het uiteindelijk toch weer af. Een blok is simpeler: een rechthoek plus een handvol stylinghints, één keer gebruikt en daarna weggegooid
Wat is een PDF-blok in PDFlibPas precies?
Een PDFlibPas-blok is een paginaannotatie waarvan /Subtype PDFlibBlock:Text, PDFlibBlock:Image of PDFlibBlock:PDF is, waarvan /Rect het placeholdergebied is en waarvan de /Contents-string name;key=value;key=value bevat. Het eerste segment is de bloknaam, alles daarna is een eigenschappenlijst. TPDFlib.AddBlock schrijft de annotatie met /F 4 (Print) en zonder appearance stream, dus een ongevuld blok neemt geen pixels in beslag op scherm of papier
De leeszijde heeft een tweede bron. Na het opsommen van de blokannotaties valt PDFlibPas terug op het paginadictionary-pad /PieceInfo << /PDFlib << /Blocks ... >> >> en zijn variant /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Page-piece dictionaries zijn standaard-PDF, gedefinieerd in ISO 32000-1 §14.5 als de plek waar een applicatie privédata bewaart die aan een pagina hangt, en daar leven blokdefinities uit externe template-authoringtools meestal. De /Blocks-container kan een dictionary zijn met de bloknaam als sleutel of een platte array; voor de arrayvorm noemt PDFlibPas de entries Block1, Block2 enzovoort, tenzij een blok zijn eigen /Name meedraagt
Binnen een page-piece blok mapt de waarde Image van /Subtype (of /Type) op een imageblok, mapt PDF of Page op een PDF-blok en wordt al het andere als tekst behandeld. Elke andere sleutel behalve /Type, /Subtype, /Name, /Rect en /Custom wordt een eigenschap, en de sleutels van een geneste /Custom-dictionary worden er ook platgeslagen, dus een entry /Custom << /department (Finance) >> is leesbaar als GetBlockProperty(Page, Index, 'department'). Blokindices zijn 1-based per pagina, annotatieblokken eerst, page-piece blokken daarna
Een bloktemplate opzetten met AddBlock
TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) voegt een blok toe aan de momenteel geselecteerde pagina en geeft 1 terug bij succes. Hij geeft 0 terug voor een lege naam, een breedte of hoogte van nul of negatief, of een BlockType anders dan 'text', 'image' of 'pdf' (hoofdletterongevoelig). Properties wordt met een puntkomma achter de naam geplakt, dus hij gebruikt dezelfde key=value;key=value-syntax als de opgeslagen string
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
// statisch artwork: rand, logo, vaste teksten ...
Lib.DrawText(72, 200, 'This certifies that');
Lib.AddBlock('text', 'recipient', 72, 230, 450, 44,
'fontname=Times-Bold;fontsize=30;fillcolor={cmyk 0 0.55 1 0.1};alignment=center');
Lib.AddBlock('text', 'course', 72, 290, 450, 24,
'fontname=Helvetica;fontsize=14;alignment=center;fitmethod=word');
Lib.AddBlock('image', 'signature', 360, 600, 160, 60, '');
Lib.AddBlock('pdf', 'seal', 72, 580, 100, 100, '');
if Lib.GetBlockCount(1) <> 4 then
raise Exception.Create('Template blocks missing');
Lib.SaveToFile('certificate-template.pdf');
finally
Lib.Free;
end;
end;
De eigenschappenparser is met opzet coulant. Sleutels worden hoofdletterongevoelig gematcht, waarden worden getrimd maar verder letterlijk teruggegeven (een waarde als {top left} komt met zijn accolades terug), en een ontbrekende sleutel geeft een lege string. De enige harde grens is de scheider: een waarde mag geen puntkomma bevatten, want de puntkomma beëindigt het segment
Hoe past FillTextBlock font, kleur en uitlijning toe?
TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) leest de blokeigenschappen, past ze toe op de tekenstatus, passt de tekst met FitTextBox in de blokrechthoek en herstelt daarna in een finally-blok het font, de tekstgrootte, de tekstkleur en de uitlijning van de aanroeper. Dat herstel telt in een merge-lus: een kopblok in rood 30 pt kan zijn stijl niet laten lekken naar de volgende DrawText-aanroep die u op dezelfde pagina doet
fontname(offont): hergebruikt eerst een font dat al onder die naam in het document is geladen, mapt daarna de Standard 14-namen zoalsHelvetica-Bold,Times-RomanofCourier, en probeert ten slotteAddTrueTypeFontmet de naamfontsize(ofsize): de tekstgrootte die aan de fitter wordt doorgegeven, die hem verkleint als de tekst niet pastfillcolor(oftextcolor,color):#RRGGBB,{rgb r g b},{cmyk c m y k},{gray g}, kale lijsten van 3 of 4 getallen, of de namen black, white, red, green, blue en gray; RGB-componenten boven 1 worden als 0–255 gelezenalignment,alignenpositionworden samen gelezen: left, right, center (of centre) en justify zetten de horizontale uitlijning, top en bottom zetten de verticale plaatsing, en standaard staat hij verticaal gecentreerdfitmethod:wordofnofitsplitvoorkomt dat lange woorden worden gebroken,scaleofexpandlaat de grootte groeien om de box te vullen
Het CMYK-pad is de reden dat drukkerijen hier iets in zien. {cmyk 0 0.55 1 0.1} roept SetTextColorCMYK aan, dus de variabele tekst wordt in DeviceCMYK uitgezonden en raakt de merkkleur op de pers zonder RGB-omweg. FillTextBlock geeft 1 terug wanneer de tekst getekend is; hij geeft 0 terug voor een lege Text, een ontbrekend blok of een blok dat geen tekstblok is, en in al die gevallen blijft het blok op zijn plek staan
Image- en PDF-blokken vullen, en de Options-bits
FillImageBlock(Page, BlockIndex, ImageID, Options) tekent een eerder toegevoegd image, en FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importeert een pagina van een ander geladen document als Form XObject en tekent die, wat vectorcontent zoals een zegel, een QR-codepagina of briefhoofd volledig vector houdt. Beide behouden de bronverhouding en centreren het resultaat binnen de blokrechthoek. Alle drie de vulfuncties delen dezelfde Options-bits: bit 1 (waarde 1) verwijdert het blok na een geslaagde vulling, door de annotatie te wissen of de entry uit de /Blocks-container te halen, en bit 2 (waarde 2) rekt image- en PDF-content uit tot de exacte rechthoek in plaats van hem te passen
function FillCertificate(const TemplateFile, SealFile, SignatureFile,
OutFile: WideString; Values: TStrings): Boolean;
var
Lib: TPDFlib;
SealID, SignatureID, I: Integer;
BlockName, BlockType: WideString;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(SealFile, '') <> 1 then Exit;
SealID := Lib.SelectedDocument; // het echte document-ID
if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
// de template is nu het geselecteerde document
SignatureID := Lib.AddImageFromFile(SignatureFile, 0);
// loop achteruit: bit 1 verwijdert elk blok, wat latere hernummert
for I := Lib.GetBlockCount(1) downto 1 do
begin
BlockName := Lib.GetBlockName(1, I);
BlockType := Lib.GetBlockType(1, I);
if BlockType = 'text' then
Lib.FillTextBlock(1, I, Values.Values[BlockName], 1)
else if (BlockType = 'image') and (SignatureID <> 0) then
Lib.FillImageBlock(1, I, SignatureID, 1)
else if BlockType = 'pdf' then
Lib.FillPDFBlock(1, I, SealID, 1, 1);
end;
Result := Lib.SaveToFile(OutFile) = 1;
finally
Lib.Free;
end;
end;
Twee details in die lus gaan makkelijk mis. De lus loopt van de hoogste index naar beneden, want het verwijderen van blok 2 maakt van het oude blok 3 blok 2; met bit 1 omhoog tellen slaat om het andere blok over. En het brondocument voor FillPDFBlock wordt eerst geladen en zijn ID wordt vastgelegd voordat de template wordt geladen, want image-ID's horen bij het geselecteerde document, dus AddImageFromFile moet draaien terwijl de template is geselecteerd. Voor batchruns waarbij het zegel of het briefhoofd per record wisselt, gebruiken de Form XObject page stitching en samenstelbare watermarktemplates in PDFlibPas hetzelfde vastleg-en-teken-mechanisme op hele pagina's
Waarom komen de blokcoördinaten gespiegeld uit?
Blokrechthoeken komen gespiegeld uit wanneer code de twee coördinatensystemen door elkaar haalt waarmee PDFlibPas werkt. Een annotatie-/Rect wordt opgeslagen in PDF user space, gemeten vanaf de onderrand van de pagina (ISO 32000-1 §12.5.2), terwijl de teken-API van TPDFlib vanaf de bovenrand meet. AddBlock neemt top-based invoer aan en slaat de onderrand op als PageHeight - Top - Height; GetBlockRect en de drie vulfuncties lezen de rechthoek terug en rekenen hem om met PageHeight - URy
In de praktijk betekent dat: gebruik GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) wanneer u de vakgeometrie nodig heeft, bijvoorbeeld om een debugkader te tekenen of een bijschrift naast een blok te zetten, en lees /Rect niet via een generieke annotatieaanroep en voer hem niet rechtstreeks in bij DrawText. Een page-piece blok met /Rect [40 700 240 730] op een letterpagina van 792 pt meldt Top = 62, niet 700. De bug die dit voorkomt is de klassieke: alle gevulde waarden komen gespiegeld uit bij de ondermarge en nergens gaat een foutmelding
Valkuilen om van te leren
De meest leerzame bug in de blokcode was een slipje van één teken. Blokannotaties worden herkend met een prefixtest op de subtype, en 'PDFlibBlock:' is 12 tekens lang. Een vroege versie vergeleek Copy(Subtype, 1, 13) met die literal van 12 tekens, wat nooit gelijk kan zijn, dus GetBlockCount gaf 0 terug voor elke template en elke vulling meldde "no such block". Er crashte niets en er werd niets gelogd. De les generaliseert naar elke template-engine: beweer in een test het blokaantal van een bekende template, want stille enumeratie zonder matches ziet er precies uit als een lege pagina
De tweede valkuil zijn document-ID's. TPDFlib.LoadFromFile geeft een statuscode terug, 1 bij succes en 0 bij falen, niet de ID van het document dat hij net heeft geladen. Die 1 als SourceDocumentID aan FillPDFBlock geven faalt, want 1 is geen geldige document-ID. Roep SelectedDocument aan vlak na een geslaagde load om de echte ID te pakken, en SelectDocument om terug te schakelen naar de template als u daarna de bron hebt geladen
Een kleinere: FillTextBlock met een lege string doet niets en houdt het blok vast. Voor een mail merge met optionele velden, zoals een ontbrekende tweede adresregel, is dat meestal precies wat u wilt, maar de ongevulde annotatie blijft dan in het uitvoerbestand staan. Verwijder hem expliciet met DeleteAnnotation of vul één spatie met bit 1 als de output schoon moet zijn. Alle blokaanroepen slaan de geselecteerde pagina op en herstellen haar, dus een vulling op pagina 3 verandert niet waar uw volgende tekenaanroep uitkomt
Wat zijn de interoperabiliteitsgrenzen van blokannotaties?
Blokannotaties zijn een privéconventie, geen standaard-PDF-feature. PDFlibBlock:Text en zijn soortgenoten staan niet tussen de annotatietypes die ISO 32000-1 §12.5.6.1 opsomt, dus een viewer behandelt ze als onbekende annotaties. Omdat de Invisible-vlag niet staat en er geen appearance stream is, tekent een conformerende viewer niets voor ze, maar sommige viewers zetten ze nog steeds op een commentarenpaneel, en andere PDF-tools begrijpen hun semantiek niet en bewaren die ook niet
- Vul met bit 1 gezet voor alles wat uw systeem verlaat, zodat het definitieve bestand alleen gewone paginacontent bevat
- PDF/A staat geen annotatietypes buiten de PDF-specificatie toe (ISO 19005-1 §6.5.2), dus gearchiveerde output mag geen ongevulde blokannotaties meedragen
- Page-piece blokken zijn veiliger om in een template te houden, want
/PieceInfois standaard en consumenten negeren data die niet van hen is - Blokken zijn geen formuliervelden: geen interactiviteit, geen veldwaarde in het bestand en geen export naar FDF of XFDF; voor data die bewerkbaar moet blijven, zie FDF-, XFDF- en XFA-formulierdata-uitwisseling in PDFlibPas
Als de template zijn variabele velden op een gestandaardiseerde, door validators controleerbare manier moet declareren voor een printworkflow, zijn blokken het verkeerde gereedschap. Daarvoor bestaat PDF/VCR-1, en het PDF/VCR-1-template-placeholdermanifest in PDFlibPas beschrijft hoe die placeholders in de structuurboom leven in plaats van in annotaties
FAQ
Kan PDFlibPas bloktemplates vullen die door andere tools zijn gemaakt?
Ja, wanneer de tool blokken opslaat onder /PieceInfo /PDFlib /Blocks of /PieceInfo /PDFlib /Private /Blocks, in dictionary- of arrayvorm, met een /Rect van vier getallen. GetBlockCount telt die na eventuele PDFlibBlock:*-annotaties, en custom sleutels worden via GetBlockProperty blootgesteld
Verandert FillTextBlock het vooraf geselecteerde font?
Nee. FillTextBlock slaat het geselecteerde font, de tekstgrootte, de tekstkleur en de uitlijning op, past de eigenschappen fontname, fontsize, fillcolor en alignment van het blok toe en herstelt daarna de originele status, ook als het passen faalt
Hoe zet ik een vectorlogo of barcodepagina in een blok?
Laad de bron-PDF, sla SelectedDocument op als zijn ID, selecteer zo nodig opnieuw het templatedocument en roep FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1) aan. De pagina wordt als Form XObject geïmporteerd, dus hij blijft vector en wordt met behoud van zijn verhouding in het blok gepast, tenzij u bit 2 toevoegt
Bloktemplates, het importeren van Form XObject-pagina's en de rest van de toolkit voor variabele data zitten in PDFlibPas, de losLab PDF Library for Delphi, die PDF-creatie, -bewerking en printproductie voor Delphi- en C++Builder-projecten dekt