Teknisk artikel

PDF-blokke i Delphi: tekst-, billed- og PDF-udfyldning

PDFlibPas (losLab PDF Library) udfylder PDF-blokskabeloner fra Delphi: en blok er et navngivet rektangel på en skabelonside, typet som tekst, billede eller PDF, og FillTextBlock, FillImageBlock og FillPDFBlock tegner variable data ind i den. Blokke findes som PDFlibBlock:Text-, PDFlibBlock:Image- og PDFlibBlock:PDF-sideannotationer eller i en page-piece-dictionary under /PieceInfo

Brugsscenariet er den kedelige, high-volume slags. En designer lægger et certifikat, en billet eller en rykker ud én gang, og programmet stempler et navn, et sædenummer, et signaturbillede eller en prærenderet stregkodeside ind i faste slots for titusindvis af records. Formularfelter kan klare en del af det, men et AcroForm-felt er et interaktivt objekt med sin egen appearance stream og sine egne regler, og du ender normalt med at flade den ud alligevel. En blok er enklere: et rektangel plus en håndfuld styling-hint, brugt én gang og så smidt væk

Hvad er præcis en PDF-blok i PDFlibPas?

En PDFlibPas-blok er en sideannotation, hvis /Subtype er PDFlibBlock:Text, PDFlibBlock:Image eller PDFlibBlock:PDF, hvis /Rect er placeholder-området, og hvis /Contents-streng rummer name;key=value;key=value. Første segment er bloknavnet, alt efter det er en egenskabsliste. TPDFlib.AddBlock skriver annotationen med /F 4 (Print) og ingen appearance stream, så en uudfyldt blok optager ingen pixels på skærm eller papir

PDFlibPas' blokskabelon-anatomi til variable data i Delphi: en sideannotation med /Subtype PDFlibBlock:Text, Image eller PDF, en /Rect-placeholder, en /Contents-streng med navn og key=value-egenskaber, /F 4 Print og ingen appearance stream, med /PieceInfo /PDFlib /Blocks page-piece-dictionary som den anden kilde
Blokke nummereres 1-baseret pr. side med annotationer først og page-piece-blokke efter dem; array-indgange bliver til Block1 og Block2, /Custom-nøgler flattes ind som egenskaber, og alt andet end Image, PDF eller Page behandles som tekst

Læsesiden har en anden kilde. Efter at have opremset blokannotationer falder PDFlibPas tilbage til side-dictionary-stien /PieceInfo << /PDFlib << /Blocks ... >> >> og dens variant /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Page-piece-dictionaries er standard-PDF, defineret i ISO 32000-1 §14.5 som stedet, hvor en applikation gemmer private data knyttet til en side, og dér lever blokdefinitioner fra eksterne skabelonværktøjer typisk. /Blocks-containeren kan være en dictionary nøglet efter bloknavn eller et rent array; i array-formen navngiver PDFlibPas indgangene Block1, Block2 og så videre, medmindre en blok bærer sit eget /Name

Inde i en page-piece-blok mapper /Subtype-værdien (eller /Type) Image til en billedblok, PDF eller Page mapper til en PDF-blok, og alt andet behandles som tekst. Alle andre nøgler undtagen /Type, /Subtype, /Name, /Rect og /Custom bliver til egenskaber, og nøglerne i en indlejret /Custom-dictionary flattes også ind, så en /Custom << /department (Finance) >>-indgang kan læses som GetBlockProperty(Page, Index, 'department'). Blokindekser er 1-baserede pr. side, annotationsblokke først, page-piece-blokke efter dem

At bygge en blokskabelon med AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) tilføjer en blok til den aktuelt valgte side og returnerer 1 ved succes. Den returnerer 0 for et tomt navn, en nul eller negativ bredde eller højde, eller en BlockType anderledes end 'text', 'image' eller 'pdf' (case-insensitiv). Properties føjes til navnet med et semikolon, så den bruger samme key=value;key=value-syntaks som den gemte streng

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // statisk grafik: kant, logo, fast tekst ...
    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;

Egenskabsparseren er bevidst afslappet. Nøgler matches case-insensitivt, værdier trimmes, men returneres ellers ordret (en værdi skrevet som {top left} kommer tilbage med sine tuborgklammer), og en manglende nøgle returnerer en tom streng. Den ene hårde grænse er separatoren: en værdi kan ikke indeholde et semikolon, fordi semikolonet afslutter segmentet

Hvordan anvender FillTextBlock font, farve og justering?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) læser blokens egenskaber, anvender dem på tegnetilstanden, får teksten til at passe ind i blokrektanglet med FitTextBox og gendanner derefter kalderens font, tekststørrelse, tekstfarve og justering i en finally-blok. Den gendannelse betyder noget i en merge-løkke: en rød 30 pt-overskriftsblok kan ikke lække sin stil videre ind i det næste DrawText-kald, du laver på samme side

  • fontname (eller font): genbruger først en font, der allerede er indlæst i dokumentet med det navn, mapper derefter Standard 14-navnene som Helvetica-Bold, Times-Roman eller Courier, og prøver til sidst AddTrueTypeFont med navnet
  • fontsize (eller size): tekststørrelsen givet videre til fitteren, som formindsker den, hvis teksten ikke passer
  • fillcolor (eller textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, bare lister med 3 eller 4 tal, eller navnene black, white, red, green, blue og gray; RGB-komponenter over 1 læses som 0–255
  • alignment, align og position læses sammen: left, right, center (eller centre) og justify sætter vandret justering, top og bottom sætter lodret placering, og default er lodret centreret
  • fitmethod: word eller nofitsplit forhindrer lange ord i at blive brudt, scale eller expand lader størrelsen vokse til at udfylde boksen

CMYK-stien er grunden til, at trykkerier bryder sig om det. {cmyk 0 0.55 1 0.1} kalder SetTextColorCMYK, så den variable tekst udsendes i DeviceCMYK og rammer brandfarven på pressen uden en RGB-omvej. FillTextBlock returnerer 1, når teksten blev tegnet; den returnerer 0 for en tom Text, en manglende blok eller en blok, der ikke er en tekstblok, og i alle de tilfælde efterlades blokken, hvor den er

FillTextBlock-behandlingsrækkefølge i PDFlibPas til Delphi: blokegenskaberne fontname, fontsize, fillcolor og alignment anvendes på tegnetilstanden, CMYK-værdien går gennem SetTextColorCMYK, teksten passes ind med FitTextBox, og kalderens font, størrelse, farve og justering kommer tilbage i en finally-blok
Blokstylingen anvendes én gang og rulles tilbage, så en rød 30 pt-overskrift ikke kan lække ind i næste DrawText; kaldet returnerer 1, når teksten blev tegnet, og 0 med blokken efterladt ved tom tekst eller en ikke-tekstblok

Udfyldning af billed- og PDF-blokke samt Options-bits

FillImageBlock(Page, BlockIndex, ImageID, Options) tegner et tidligere tilføjet billede, og FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importerer en side fra et andet indlæst dokument som et Form XObject og tegner den, hvilket holder vektorindhold som et segl, en QR-kodeside eller et brevhoved fuldt vektorielt. Begge bevarer kildens aspektforhold og centrerer resultatet inde i blokrektanglet. Alle tre udfyldningsfunktioner deler de samme Options-bits: bit 1 (værdi 1) fjerner blokken efter en vellykket udfyldning, enten ved at slette annotationen eller fjerne indgangen fra /Blocks-containeren, og bit 2 (værdi 2) strækker billed- og PDF-indhold til det eksakte rektangel i stedet for at passe det ind

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 rigtige dokument-ID
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // skabelonen er nu det valgte dokument
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // gå baglæns: bit 1 fjerner hver blok, hvilket renummererer de senere
    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;

To detaljer i den løkke er lette at få forkert. Løkken kører fra det højeste indeks og ned, fordi fjernelse af blok 2 gør den gamle blok 3 til blok 2; at tælle opad med bit 1 sat springer hver anden blok over. Og kildedokumentet til FillPDFBlock indlæses først, og dets ID fanges, før skabelonen indlæses, fordi billed-ID'er tilhører det valgte dokument, så AddImageFromFile skal køre, mens skabelonen er valgt. Til batchkørsler, hvor seglet eller brevhovedet skifter pr. record, bruger Form XObject-sidesammenføjningen og de sammensætbare vandmærkeskabeloner i PDFlibPas samme fang-og-tegn-mekanisme på hele sider

Hvorfor kommer blokkoordinaterne ud vendt om?

Blokrektangler kommer ud vendt om, når kode blander de to koordinatsystemer, PDFlibPas omgås. En annotations /Rect gemmes i PDF user space, målt fra sidens bund (ISO 32000-1 §12.5.2), mens TPDFlib-tegne-API'en måler fra toppen. AddBlock tager topbaseret input og gemmer den nederste kant som PageHeight - Top - Height; GetBlockRect og de tre udfyldningsfunktioner læser rektanglet tilbage og konverterer det med PageHeight - URy

I praksis betyder det: brug GetBlockRect(Page, BlockIndex, Left, Top, Width, Height), når du skal bruge slot-geometrien, for eksempel til at tegne en debug-ramme eller placere en tekst ved siden af en blok, og læs ikke /Rect gennem et generisk annotationskald og fodr det direkte ind i DrawText. En page-piece-blok med /Rect [40 700 240 730] på en 792 pt letter-side rapporterer Top = 62, ikke 700. Fejlen, det forhindrer, er den klassiske, hvor hver udfyldt værdi lander spejlet nær bundmargenen uden nogen fejl

Blokrektangel-koordinatflip i PDFlibPas til Delphi: /Rect-værdier lever i bundbaseret PDF user space, mens AddBlock tager topbaseret input og gemmer PageHeight minus Top minus Height, så /Rect 40 700 240 730 på en 792 pt letter-side læses tilbage som Top 62 gennem GetBlockRect og dets PageHeight minus URy-konvertering
At blande de to koordinatsystemer spejler hver udfyldt værdi mod bundmargenen uden en eneste fejl; læs slot-geometrien gennem GetBlockRect i stedet for at fodre /Rect direkte ind i DrawText

Faldgruber værd at lære af

Den mest lærerige fejl i blokkoden var en fejl på ét enkelt tegn. Blokannotationer genkendes af en præfiks-test på subtypen, og 'PDFlibBlock:' er 12 tegn lang. En tidlig version sammenlignede Copy(Subtype, 1, 13) med det 12 tegn lange literal, som aldrig kan være lig, så GetBlockCount returnerede 0 for hver skabelon, og hver udfyldning rapporterede "no such block". Intet crashede, og intet blev logget. Lektionen generaliserer til enhver skabelonmotor: assert blokantallet for en kendt skabelon i en test, fordi lydløs nul-match-opremsning ser præcis ud som en tom side

Den anden faldgrube er dokument-ID'er. TPDFlib.LoadFromFile returnerer en statuskode, 1 for succes og 0 for fiasko, ikke ID'et på dokumentet, den lige indlæste. At give dén 1 med som SourceDocumentID til FillPDFBlock fejler, fordi 1 ikke er et gyldigt dokument-ID. Kald SelectedDocument lige efter en vellykket indlæsning for at få det rigtige ID, og SelectDocument for at skifte tilbage til skabelonen, hvis du indlæste kilden bagefter

En mindre en: FillTextBlock med en tom streng gør intet og beholder blokken. Til et mail merge med valgfrie felter, som en manglende anden adresselinje, er det som regel det, du vil, men den uudfyldte annotation bliver så i outputfilen. Fjern den eksplicit med DeleteAnnotation eller udfyld et enkelt mellemrum med bit 1, hvis outputtet skal være rent. Alle blokkald gemmer og gendanner den valgte side, så en udfyldning på side 3 ikke ændrer, hvor dit næste tegnekald lander

Hvad er grænserne for blokannotationers interoperabilitet?

Blokannotationer er en privat konvention, ikke en standard-PDF-funktion. PDFlibBlock:Text og søskendene er ikke blandt annotationstyperne opført i ISO 32000-1 §12.5.6.1, så en viewer behandler dem som ukendte annotationer. Fordi Invisible-flagget ikke er sat, og der ikke er nogen appearance stream, tegner konforme viewers intet for dem, men nogle viewers kan stadig vise dem i et kommentarpanel, og andre PDF-værktøjer vil hverken forstå eller bevare deres semantik

  • Udfyld med bit 1 sat til alt, der forlader dit system, så den endelige fil kun indeholder almindeligt sideindhold
  • PDF/A tillader ikke annotationstyper uden for PDF-specifikationen (ISO 19005-1 §6.5.2), så et arkiveret output må ikke bære uudfyldte blokannotationer
  • Page-piece-blokke er sikrere at beholde i en skabelon, da /PieceInfo er standard, og forbrugere ignorerer data, de ikke ejer
  • Blokke er ikke formularfelter: ingen interaktivitet, ingen feltværdi i filen og ingen eksport til FDF eller XFDF; til data, der skal forblive redigerbare, se FDF-, XFDF- og XFA-formulardataudveksling i PDFlibPas

Skal skabelonen erklære sine variable felter på en standardiseret, validator-tjekbar måde til et print-workflow, er blokke det forkerte værktøj. Dét er PDF/VCR-1 til, og PDF/VCR-1-skabelonens placeholder-manifest i PDFlibPas dækker, hvordan de placeholders lever i strukturtræet i stedet for i annotationer

FAQ

Kan PDFlibPas udfylde blokskabeloner skabt af andre værktøjer?

Ja, når værktøjet gemmer blokke under /PieceInfo /PDFlib /Blocks eller /PieceInfo /PDFlib /Private /Blocks, i dictionary- eller array-form, med en /Rect af fire tal. GetBlockCount tæller dem efter eventuelle PDFlibBlock:*-annotationer, og custom-nøgler eksponeres gennem GetBlockProperty

Ændrer FillTextBlock den font, jeg valgte før kaldet?

Nej. FillTextBlock gemmer den valgte font, tekststørrelse, tekstfarve og justering, anvender blokkens fontname-, fontsize-, fillcolor- og alignment-egenskaber og gendanner den oprindelige tilstand bagefter, selv hvis tilpasningen fejler

Hvordan placerer jeg et vektorlogo eller en stregkodeside i en blok?

Indlæs kilde-PDF'en, gem SelectedDocument som dens ID, vælg skabelondokumentet igen om nødvendigt, og kald FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). Siden importeres som et Form XObject, så den forbliver vektor og passes ind i blokken med bevaret aspektforhold, medmindre du tilføjer bit 2

Blokkskabeloner, Form XObject-sideimport og resten af værktøjskassen til variable data følger med i PDFlibPas, losLab PDF Library for Delphi, som dækker PDF-oprettelse, redigering og printproduktion til Delphi- og C++Builder-projekter