Teknisk artikel

PDF-blockmallar i Delphi: text-, bild- och PDF-fyllningar

PDFlibPas (losLab PDF Library) fyller PDF-blockmallar från Delphi: ett block är en namngiven rektangel på en mallsida, typad som text, bild eller PDF, och FillTextBlock, FillImageBlock och FillPDFBlock ritar variabel data i den. Block hittas som sidannoteringar av typen PDFlibBlock:Text, PDFlibBlock:Image och PDFlibBlock:PDF, eller i en page-piece-ordbok under /PieceInfo

Användningsfallet är den tråkiga, högvolymiga sorten. En formgivare lägger ut ett certifikat, en eventbiljett eller en påminnelse en gång, och programmet stämplar ett namn, ett platsnummer, en signaturbild eller en förrenderad streckkodssida i fasta luckor för tiotusen poster. Formulärfält kan göra en del av det, men ett AcroForm-fält är ett interaktivt objekt med sin egen utseendeström och sina egna regler, och du hamnar oftast i att platta ut det ändå. Ett block är enklare: en rektangel plus ett fåtal formgivningsanvisningar, konsumerat en gång och sedan kastat

Vad är egentligen ett PDF-block i PDFlibPas?

Ett PDFlibPas-block är en sidannotering vars /Subtype är PDFlibBlock:Text, PDFlibBlock:Image eller PDFlibBlock:PDF, vars /Rect är platshållarytan, och vars /Contents-sträng håller name;key=value;key=value. Det första segmentet är blocknamnet, allt efter det är en egenskapslista. TPDFlib.AddBlock skriver annoteringen med /F 4 (Print) och ingen utseendeström, så ett ofyllt block upptar inga pixlar på skärm eller papper

Anatomin i en PDFlibPas-blockmall för variabel data i Delphi: en sidannotering med /Subtype PDFlibBlock:Text, Image eller PDF, en /Rect-platshållare, en /Contents-sträng med namn och key=value-egenskaper, /F 4 Print och ingen utseendeström, med page-piece-ordboken /PieceInfo /PDFlib /Blocks som den andra källan
Block räknas upp 1-baserat per sida med annoteringar först och page-piece-block efter dem; arrayposter blir Block1 och Block2, /Custom-nycklar plattas ut till egenskaper, och allt utom Image, PDF eller Page behandlas som text

Lässidan har en andra källa. Efter uppräkning av blockannoteringar faller PDFlibPas tillbaka på sidordboksvägen /PieceInfo << /PDFlib << /Blocks ... >> >> och dess variant /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Page-piece-ordböcker är standard-PDF, definierade i ISO 32000-1 §14.5 som platsen där ett program lagrar privat data fäst vid en sida, och där brukar blockdefinitioner från externa mallverktyg leva. /Blocks-behållaren får vara en ordbok nycklad på blocknamn eller en vanlig array; för arrayformen namnger PDFlibPas posterna Block1, Block2 och så vidare, om inte ett block bär sitt eget /Name

Inuti ett page-piece-block mappas /Subtype- (eller /Type)-värdet Image till ett bildblock, PDF eller Page till ett PDF-block, och allt annat behandlas som text. Varje annan nyckel utöver /Type, /Subtype, /Name, /Rect och /Custom blir en egenskap, och nycklarna i en nästlad /Custom-ordbok plattas också ut, så att en post /Custom << /department (Finance) >> är läsbar som GetBlockProperty(Page, Index, 'department'). Blockindex är 1-baserade per sida, annoteringsblock först, page-piece-block efter dem

Att skriva en blockmall med AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) lägger till ett block på den valda sidan och returnerar 1 vid framgång. Den returnerar 0 för ett tomt namn, en bredd eller höjd som är noll eller negativ, eller en BlockType annan än 'text', 'image' eller 'pdf' (skiftlägesokänsligt). Properties fogas till namnet med ett semikolon, så den använder samma key=value;key=value-syntax som den lagrade strängen

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // statisk grafik: ram, logotyp, fast text ...
    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;

Egenskapsparsaren är medvetet lös. Nycklar matchas skiftlägesokänsligt, värden trimmas men returneras i övrigt ordagrant (ett värde skrivet som {top left} kommer tillbaka med sina klamrar), och en saknad nyckel returnerar en tom sträng. Den enda hårda gränsen är avgränsaren: ett värde får inte innehålla semikolon, eftersom semikolonet avslutar segmentet

Hur tillämpar FillTextBlock typsnitt, färg och justering?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) läser blockegenskaperna, tillämpar dem på rittillståndet, anpassar texten till blockrektangeln med FitTextBox, och återställer sedan anroparens typsnitt, textstorlek, textfärg och justering i ett finally-block. Den återställningen spelar roll i en sammanslagningsloop: ett rött 30 pt-rubrikblock får inte läcka sin stil in i nästa DrawText-anrop du gör på samma sida

  • fontname (eller font): återanvänder först ett typsnitt redan laddat i dokumentet med det namnet, mappar sedan Standard 14-namnen som Helvetica-Bold, Times-Roman eller Courier, och försöker till sist AddTrueTypeFont med namnet
  • fontsize (eller size): textstorleken som skickas till anpassaren, som krymper den om texten inte får plats
  • fillcolor (eller textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, blotta listor med 3 eller 4 tal, eller namnen black, white, red, green, blue och gray; RGB-komponenter över 1 läses som 0–255
  • alignment, align och position läses tillsammans: left, right, center (eller centre) och justify sätter horisontell justering, top och bottom sätter vertikal placering, och standardläget är vertikalt centrerat
  • fitmethod: word eller nofitsplit hindrar långa ord från att brytas, scale eller expand låter storleken växa för att fylla rutan

CMYK-vägen är skälet att tryckerier bryr sig. {cmyk 0 0.55 1 0.1} anropar SetTextColorCMYK, så den variabla texten sänds ut i DeviceCMYK och träffar varumärkesfärgen på pressen utan en omväg över RGB. FillTextBlock returnerar 1 när texten ritades; den returnerar 0 för en tom Text, ett saknat block, eller ett block som inte är ett textblock, och i alla de fallen lämnas blocket kvar

Bearbetningsordning i FillTextBlock i PDFlibPas för Delphi: blockegenskaperna fontname, fontsize, fillcolor och alignment tillämpas på rittillståndet, CMYK-värdet går via SetTextColorCMYK, texten anpassas med FitTextBox, och anroparens typsnitt, storlek, färg och justering kommer tillbaka i ett finally-block
Blockformgivning tillämpas en gång och rullas tillbaka så att ett rött 30 pt-rubrikblock inte kan läcka in i nästa DrawText; anropet returnerar 1 när text ritades och 0 med blocket lämnat kvar för tom text eller ett icke-textblock

Att fylla bild- och PDF-block, och Options-bitarna

FillImageBlock(Page, BlockIndex, ImageID, Options) ritar en tidigare tillagd bild, och FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importerar en sida ur ett annat laddat dokument som ett Form XObject och ritar den, vilket håller vektorinnehåll som ett sigill, en QR-kodsida eller ett brevhuvud fullt vektoriellt. Båda bevarar källans bildförhållande och centrerar resultatet inne i blockrektangeln. Alla tre fyllnadsfunktionerna delar samma Options-bitar: bit 1 (värde 1) tar bort blocket efter en lyckad fyllning, antingen genom att radera annoteringen eller genom att ta bort posten ur /Blocks-behållaren, och bit 2 (värde 2) sträcker bild- och PDF-innehåll till exakt rektangeln i stället för att anpassa det

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;          // det riktiga dokument-ID:t
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // mallen är nu det valda dokumentet
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // gå baklänges: bit 1 tar bort varje block, vilket omnumrerar senare
    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;

Två detaljer i den loopen är lätta att få fel. Loopen går från högsta index nedåt eftersom att ta bort block 2 gör gamla block 3 till block 2; att räkna uppåt med bit 1 satt hoppar över vartannat block. Och källdokumentet för FillPDFBlock laddas först och dess ID fångas innan mallen laddas, eftersom bild-ID:n tillhör det valda dokumentet, så AddImageFromFile måste köras medan mallen är vald. För batchkörningar där sigillet eller brevhuvudet byter per post använder Form XObject-sidsammanfogningen och sammansättningsbara vattenstämpelmallar i PDFlibPas samma fånga-och-rita-mekanism på hela sidor

Varför kommer blockkoordinaterna ut spegelvända?

Blockrektanglar kommer ut spegelvända när kod blander de två koordinatsystem PDFlibPas har att göra med. En annoteringens /Rect lagras i PDF-användarutrymme, mätt från sidans botten (ISO 32000-1 §12.5.2), medan rit-API:et TPDFlib mäter från toppen. AddBlock tar toppbaserad indata och lagrar undre kanten som PageHeight - Top - Height; GetBlockRect och de tre fyllnadsfunktionerna läser tillbaka rektangeln och konverterar den med PageHeight - URy

I praktiken betyder det: använd GetBlockRect(Page, BlockIndex, Left, Top, Width, Height) när du behöver luckans geometri, till exempel för att rita en avlusningsram eller placera en bildtext bredvid ett block, och läs inte /Rect via ett generiskt annoteringsanrop och mata in den rakt i DrawText. Ett page-piece-block med /Rect [40 700 240 730] på en 792 pt Lettersida rapporterar Top = 62, inte 700. Felet detta förhindrar är det klassiska där varje ifyllt värde hamnar speglat nära bottenmarginalen utan att något rapporteras

Koordinatvändning för blockrektangel i PDFlibPas för Delphi: /Rect-värden lever i bottenbaserat PDF-användarutrymme medan AddBlock tar toppbaserad indata och lagrar PageHeight minus Top minus Height, så /Rect 40 700 240 730 på en 792 pt Lettersida läses tillbaka som Top 62 via GetBlockRect och dess PageHeight minus URy-konvertering
Att blanda de två koordinatsystemen speglar varje ifyllt värde mot bottenmarginalen utan ett enda fel; läs luckans geometri via GetBlockRect i stället för att mata /Rect rakt in i DrawText

Fallgropar värda att lära av

Det mest lärorika felet i blockkoden var en en-teckens förskrivning. Blockannoteringar känns igen med ett prefixtest på subtypen, och 'PDFlibBlock:' är 12 tecken långt. En tidig version jämförde Copy(Subtype, 1, 13) mot den 12-teckensliteralen, vilket aldrig kan bli lika, så GetBlockCount returnerade 0 för varje mall och varje fyllning rapporterade "no such block". Inget krashade och inget loggades. Lärdomen generaliseras till vilken mallmotor som helst: gör en kontroll av en känd malls blockantal i ett test, för tyst uppräkning med noll träffar ser exakt ut som en tom sida

Den andra fällan är dokument-ID:n. TPDFlib.LoadFromFile returnerar en statuskod, 1 för framgång och 0 för misslyckande, inte ID:t på dokumentet den just laddade. Att skicka den 1:an som SourceDocumentID till FillPDFBlock misslyckas eftersom 1 inte är ett giltigt dokument-ID. Anropa SelectedDocument direkt efter en lyckad laddning för att få det riktiga ID:t, och SelectDocument för att växla tillbaka till mallen om du laddade källan efteråt

En mindre: FillTextBlock med en tom sträng gör ingenting och behåller blocket. För ett mail merge med valfria fält, som en saknad andra adressrad, är det oftast vad du vill, men den ofyllda annoteringen blir då kvar i utdatafilen. Ta bort den explicit med DeleteAnnotation eller fyll ett enskilt mellanslag med bit 1 om utdata måste vara rent. Alla blockanrop sparar och återställer den valda sidan, så en fyllning på sida 3 ändrar inte vart nästa ritningsanrop går

Vilka är interoperabilitetsgränserna för blockannoteringar?

Blockannoteringar är en privat konvention, inte en standardfunktion i PDF. PDFlibBlock:Text och dess syskon finns inte bland annoteringstyperna listade i ISO 32000-1 §12.5.6.1, så en visare behandlar dem som okända annoteringar. Eftersom Invisible-flaggan är nollställd och det inte finns någon utseendeström ritar konforma visare ingenting för dem, men vissa visare kan ändå lista dem i en kommentarspanel, och andra PDF-verktyg varken förstår eller bevarar deras semantik

  • Fyll med bit 1 satt för allt som lämnar ditt system, så att den slutliga filen bara innehåller vanligt sidinnehåll
  • PDF/A tillåter inte annoteringstyper utanför PDF-specifikationen (ISO 19005-1 §6.5.2), så ett arkiverat utdata får inte bära ofyllda blockannoteringar
  • Page-piece-block är säkrare att behålla i en mall, eftersom /PieceInfo är standard och konsumenter ignorerar data de inte äger
  • Block är inte formulärfält: ingen interaktivitet, inget fältvärde i filen och ingen export till FDF eller XFDF; för data som måste förbli redigerbar, se FDF-, XFDF- och XFA-formulärdatautbyte i PDFlibPas

Om mallen måste deklarera sina variabla fält på ett standardiserat, validatorerbart sätt för ett tryckeriarbetsflöde är block fel verktyg. Det är det PDF/VCR-1 är till för, och PDF/VCR-1-mallens platshållarmanifest i PDFlibPas täcker hur de platshållarna lever i strukturträdet i stället för i annoteringar

FAQ

Kan PDFlibPas fylla blockmallar skapade av andra verktyg?

Ja, när verktyget lagrar block under /PieceInfo /PDFlib /Blocks eller /PieceInfo /PDFlib /Private /Blocks, i ordboks- eller arrayform, med en /Rect av fyra tal. GetBlockCount räknar de efter eventuella PDFlibBlock:*-annoteringar, och anpassade nycklar exponeras via GetBlockProperty

Ändrar FillTextBlock typsnittet jag valde före anropet?

Nej. FillTextBlock sparar det valda typsnittet, textstorleken, textfärgen och justeringen, tillämpar blockets egenskaper fontname, fontsize, fillcolor och alignment, och återställer originaltillståndet efteråt, även om anpassningen misslyckas

Hur placerar jag en vektorlogotyp eller streckkodssida i ett block?

Ladda käll-PDF:en, lagra SelectedDocument som dess ID, välj malldokumentet igen vid behov, och anropa FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). Sidan importeras som ett Form XObject, så den förblir vektoriell och anpassas till blocket med bevarat bildförhållande om du inte lägger till bit 2

Blockmallar, Form XObject-sidimport och resten av verktygslådan för variabel data ingår i PDFlibPas, losLab PDF Library for Delphi, som täcker PDF-skapande, redigering och tryckproduktion för Delphi- och C++Builder-projekt