Technisch artikel

PDF-bloktemplates in Delphi: tekst, image en PDF vullen

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

Anatomie van de PDFlibPas-bloktemplate voor Delphi-variabele data: een paginaannotatie met /Subtype PDFlibBlock:Text, Image of PDF, een /Rect-placeholder, een /Contents-string met naam en key=value-eigenschappen, /F 4 Print en geen appearance stream, met de page-piece dictionary /PieceInfo /PDFlib /Blocks als tweede bron
Blokken worden per pagina 1-based opgesomd, annotaties eerst en page-piece blokken daarna; array-entries worden Block1 en Block2, /Custom-sleutels worden platgeslagen tot eigenschappen, en alles behalve Image, PDF of Page wordt als tekst behandeld

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 (of font): hergebruikt eerst een font dat al onder die naam in het document is geladen, mapt daarna de Standard 14-namen zoals Helvetica-Bold, Times-Roman of Courier, en probeert ten slotte AddTrueTypeFont met de naam
  • fontsize (of size): de tekstgrootte die aan de fitter wordt doorgegeven, die hem verkleint als de tekst niet past
  • fillcolor (of textcolor, 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 gelezen
  • alignment, align en position worden 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 gecentreerd
  • fitmethod: word of nofitsplit voorkomt dat lange woorden worden gebroken, scale of expand laat 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

Verwerkingsvolgorde van FillTextBlock in PDFlibPas voor Delphi: de blokeigenschappen fontname, fontsize, fillcolor en alignment worden op de tekenstatus toegepast, de CMYK-waarde loopt via SetTextColorCMYK, de tekst wordt met FitTextBox gepast en het font, de grootte, de kleur en de uitlijning van de aanroeper komen in een finally-blok terug
Blokstyling wordt één keer toegepast en teruggedraaid zodat een kop in rood 30 pt niet kan lekken naar de volgende DrawText; de aanroep geeft 1 terug wanneer tekst is getekend en 0 met het blok op zijn plek bij lege tekst of een niet-tekstblok

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

Coördinaatspiegeling van de blokrechthoek in PDFlibPas voor Delphi: /Rect-waarden staan in bottom-based PDF user space terwijl AddBlock top-based invoer neemt en PageHeight minus Top minus Height opslaat, dus /Rect 40 700 240 730 op een letterpagina van 792 pt leest via GetBlockRect en zijn omrekening PageHeight minus URy terug als Top 62
De twee coördinatensystemen door elkaar halen spiegelt elke gevulde waarde richting ondermarge zonder één foutmelding; lees de vakgeometrie via GetBlockRect in plaats van /Rect rechtstreeks in DrawText te voeren

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 /PieceInfo is 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