Technický článek

Šablony PDF bloků v Delphi: výplně textem, obrázkem a PDF

PDFlibPas (losLab PDF Library) plní šablony PDF bloků z Delphi: blok je pojmenovaný obdélník na stránce šablony, typovaný jako text, obrázek nebo PDF, a FillTextBlock, FillImageBlock a FillPDFBlock do něj kreslí variabilní data. Bloky se hledají jako anotace stránky PDFlibBlock:Text, PDFlibBlock:Image a PDFlibBlock:PDF, nebo ve slovníku page-piece pod /PieceInfo

Use case je ten nudný, vysokosvazkový druh. Designér vysází certifikát, vstupenku na akci nebo výzvu k úhradě jednou a program na deset tisíc záznamů pečetí do pevných slotů jméno, číslo sedadla, obrázek podpisu nebo předrenderovanou stránku s čárovým kódem. Formulářová pole zvládnou část toho, ale AcroForm pole je interaktivní objekt s vlastním appearance streamem a vlastními pravidly a stejně s ním obvykle skončíte u flattening. Blok je jednodušší: obdélník plus hrst stylizačních nápověd, spotřebuje se jednou a zahodí

Co přesně je PDF blok v PDFlibPas?

Blok PDFlibPas je anotace stránky, jejíž /Subtype je PDFlibBlock:Text, PDFlibBlock:Image nebo PDFlibBlock:PDF, jejíž /Rect je oblast placeholderu a jejíž řetězec /Contents drží name;key=value;key=value. První segment je jméno bloku, všechno za ním je seznam vlastností. TPDFlib.AddBlock zapisuje anotaci s /F 4 (Print) a bez appearance streamu, takže nevyplněný blok nezabírá žádné pixely na obrazovce ani na papíře

Anatomie šablony bloku PDFlibPas pro variabilní data v Delphi: anotace stránky s /Subtype PDFlibBlock:Text, Image nebo PDF, placeholder /Rect, řetězec /Contents se jménem a vlastnostmi key=value, /F 4 Print a žádný appearance stream, se slovníkem page-piece /PieceInfo /PDFlib /Blocks jako druhým zdrojem
Bloky se číslují od 1 na stránku, nejdřív anotace a za nimi bloky page-piece; položky pole se stanou Block1 a Block2, klíče /Custom se zploští do vlastností a cokoli jiného než Image, PDF nebo Page se bere jako text

Strana čtení má druhý zdroj. Po sečtení blokových anotací sáhne PDFlibPas po cestě slovníku stránky /PieceInfo << /PDFlib << /Blocks ... >> >> a její variantě /PieceInfo << /PDFlib << /Private << /Blocks ... >> >> >>. Slovníky page-piece jsou standardní PDF, definované v ISO 32000-1 §14.5 jako místo, kam aplikace ukládá privátní data připojená ke stránce, a právě tam obvykle žijí definice bloků z externích nástrojů na autoring šablon. Kontejner /Blocks může být slovník klíčovaný jmény bloků nebo prosté pole; pro formu pole pojmenovává PDFlibPas položky Block1, Block2 a tak dále, pokud blok nenese vlastní /Name

Uvnitř bloku page-piece se hodnota /Subtype (nebo /Type) Image mapuje na blok obrázku, PDF nebo Page na blok PDF a cokoli jiného se bere jako text. Každý další klíč kromě /Type, /Subtype, /Name, /Rect a /Custom se stane vlastností a klíče vnořeného slovníku /Custom se zploští také, takže položka /Custom << /department (Finance) >> se čte jako GetBlockProperty(Page, Index, 'department'). Indexy bloků se počítají od 1 na stránku, nejdřív bloky anotací, za nimi bloky page-piece

Autoring šablony bloků přes AddBlock

TPDFlib.AddBlock(BlockType, BlockName, Left, Top, Width, Height, Properties) přidá blok na aktuálně vybranou stránku a vrací 1 při úspěchu. Vrací 0 pro prázdné jméno, nulovou nebo zápornou šířku či výšku, nebo BlockType jiný než 'text', 'image' nebo 'pdf' (nerozlišuje velikost písmen). Properties se připojí ke jménu středníkem, takže používá tutéž syntaxi key=value;key=value jako uložený řetězec

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // statická grafika: rámeček, logo, pevné texty ...
    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;

Parser vlastností je záměrně shovívavý. Klíče se porovnávají bez ohledu na velikost písmen, hodnoty se ořezou, ale jinak se vracejí slovo od slova (hodnota zapsaná jako {top left} se vrátí se svými složenými závorkami) a chybějící klíč vrátí prázdný řetězec. Jediný tvrdý limit je oddělovač: hodnota nesmí obsahovat středník, protože středník segment ukončuje

Jak FillTextBlock aplikuje font, barvu a zarovnání?

TPDFlib.FillTextBlock(Page, BlockIndex, Text, Options) přečte vlastnosti bloku, aplikuje je na kreslicí stav, vmístí text do obdélníku bloku přes FitTextBox a potom v bloku finally obnoví font volajícího, velikost textu, barvu textu a zarovnání. To obnovení záleží ve smyčce merge: červený nadpisový blok 30 pt nemůže prosakovat svým stylem do dalšího volání DrawText, které na téže stránce uděláte

  • fontname (nebo font): nejdřív znovu použije font už načtený v dokumentu pod tímto jménem, pak mapuje jména Standard 14 jako Helvetica-Bold, Times-Roman nebo Courier a nakonec zkusí AddTrueTypeFont s tímto jménem
  • fontsize (nebo size): velikost textu předaná fitteru, který ji zmenší, když se text nevejde
  • fillcolor (nebo textcolor, color): #RRGGBB, {rgb r g b}, {cmyk c m y k}, {gray g}, holé seznamy 3 nebo 4 čísel, nebo jména black, white, red, green, blue a gray; složky RGB nad 1 se čtou jako 0–255
  • alignment, align a position se čtou společně: left, right, center (nebo centre) a justify nastavují vodorovné zarovnání, top a bottom nastavují svislé umístění a výchozí stav je svisle na střed
  • fitmethod: word nebo nofitsplit zabrání lámání dlouhých slov, scale nebo expand nechá velikost narůst, aby vyplnila box

CMYK cesta je důvod, proč se o to zajímají tiskárny. {cmyk 0 0.55 1 0.1} zavolá SetTextColorCMYK, takže variabilní text se vysílá v DeviceCMYK a trefí firemní barvu na tisku bez okliky přes RGB. FillTextBlock vrací 1, když se text nakreslil; vrací 0 pro prázdný Text, chybějící blok nebo blok, který není textovým blokem, a ve všech těch případech zůstává blok na místě

Pořadí zpracování FillTextBlock v PDFlibPas pro Delphi: vlastnosti bloku fontname, fontsize, fillcolor a alignment se aplikují na kreslicí stav, hodnota CMYK jde přes SetTextColorCMYK, text se vmístí přes FitTextBox a font, velikost, barva a zarovnání volajícího se vrátí v bloku finally
Stylování bloku se aplikuje jednou a zase odroluje, takže červený nadpis 30 pt nemůže prosakovat do dalšího DrawText; volání vrací 1, když se text nakreslil, a 0 s blokem ponechaným na místě pro prázdný text nebo netextový blok

Plnění bloků obrázku a PDF a bity Options

FillImageBlock(Page, BlockIndex, ImageID, Options) nakreslí dříve přidaný obrázek a FillPDFBlock(Page, BlockIndex, SourceDocumentID, SourcePage, Options) importuje stránku jiného načteného dokumentu jako Form XObject a nakreslí ji, což udrží vektorový obsah jako razítko, stránku s QR kódem nebo hlavičku dopisu plně vektorovou. Obojí zachovává poměr stran zdroje a vycentruje výsledek uvnitř obdélníku bloku. Všechny tři fill funkce sdílejí stejné bity Options: bit 1 (hodnota 1) odstraní blok po úspěšném plnění, buď smazáním anotace, nebo odebráním položky z kontejneru /Blocks, a bit 2 (hodnota 2) natáhne obsah obrázku a PDF na přesný obdélník místo vymístění

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;          // skutečné ID dokumentu
    if Lib.LoadFromFile(TemplateFile, '') <> 1 then Exit;
    // šablona je teď vybraným dokumentem
    SignatureID := Lib.AddImageFromFile(SignatureFile, 0);

    // jděte pozpátku: bit 1 odstraňuje každý blok, čímž přečísluje pozdější
    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;

Dva detaily v té smyčce se snadno pokazí. Smyčka jede od nejvyššího indexu dolů, protože odstranění bloku 2 promění starý blok 3 v blok 2; počítání nahoru s nastaveným bitem 1 přeskočí každý druhý blok. A zdrojový dokument pro FillPDFBlock se načítá první a jeho ID se chytí před načtením šablony, protože ID obrázků patří vybranému dokumentu, takže AddImageFromFile musí běžet, když je vybraná šablona. Pro dávkové běhy, kde se razítko nebo hlavička mění po záznamech, používá stitching stránek Form XObject a skládací šablony vodoznaků v PDFlibPas tentýž mechanismus chycení a nakreslení na celé stránky

Proč vyjdou souřadnice bloků převrácené?

Obdélníky bloků vyjdou převrácené, když kód promíchá dva souřadné systémy, se kterými PDFlibPas pracuje. Anotace /Rect se ukládá v PDF user space, měřená od spodku stránky (ISO 32000-1 §12.5.2), zatímco kreslicí API TPDFlib měří odshora. AddBlock bere vstup odshora a ukládá dolní hranu jako PageHeight - Top - Height; GetBlockRect a tři fill funkce čtou obdélník zpět a převádějí ho přes PageHeight - URy

V praxi to znamená: použijte GetBlockRect(Page, BlockIndex, Left, Top, Width, Height), když potřebujete geometrii slotu, třeba na nakreslení debug rámečku nebo umístění popisku vedle bloku, a nečtěte /Rect přes generické volání anotací, abyste ho nasypali přímo do DrawText. Page-piece blok s /Rect [40 700 240 730] na letter stránce 792 pt hlásí Top = 62, ne 700. Bug, kterému to předchází, je ten klasický, kdy každá vyplněná hodnota dopadne zrcadlově u dolního okraje a nic nehlásí chybu

Převrácení souřadnic obdélníku bloku v PDFlibPas pro Delphi: hodnoty /Rect žijí v PDF user space počítaném odspodu, zatímco AddBlock bere vstup odshora a ukládá PageHeight minus Top minus Height, takže /Rect 40 700 240 730 na letter stránce 792 pt se přes GetBlockRect a jeho převod PageHeight minus URy čte zpět jako Top 62
Promíchání obou souřadných systémů zrcadlí každou vyplněnou hodnotu k dolnímu okraji bez jediné chyby; geometrii slotu čtěte přes GetBlockRect, místo abyste /Rect sypali přímo do DrawText

Pasti, ze kterých se dá učit

Nejpoučnější bug v blokovém kódu byl přešlap o jeden znak. Blokové anotace se poznávají testem prefixu subtype a 'PDFlibBlock:' je dlouhý 12 znaků. Raná verze porovnávala Copy(Subtype, 1, 13) proti tomuto 12znakovému literálu, což se nikdy nemůže rovnat, takže GetBlockCount vracel 0 pro každou šablonu a každé plnění hlásilo „no such block". Nic nespadlo a nic se nelogovalo. Lekce se zobecňuje na jakýkoli template engine: otestujte počet bloků známé šablony v testu, protože tiché vyčíslení bez zásahů vypadá přesně jako prázdná stránka

Druhá past jsou ID dokumentů. TPDFlib.LoadFromFile vrací status kód, 1 pro úspěch a 0 pro selhání, ne ID dokumentu, který právě načetl. Předání té jedničky jako SourceDocumentID do FillPDFBlock selže, protože 1 není platné ID dokumentu. Zavolejte SelectedDocument hned po úspěšném načtení, abyste dostali skutečné ID, a SelectDocument pro přepnutí zpět na šablonu, pokud jste zdroj načetli poté

Menší z nich: FillTextBlock s prázdným řetězcem nedělá nic a blok ponechá. Pro mail merge s volitelnými poli, třeba chybějícím druhým řádkem adresy, je to obvykle to, co chcete, ale nevyplněná anotace pak zůstává ve výstupním souboru. Odstraňte ji explicitně přes DeleteAnnotation, nebo vyplňte jednu mezeru s bitem 1, když musí být výstup čistý. Všechna bloková volání ukládají a obnovují vybranou stránku, takže plnění na stránce 3 nezmění, kam míří vaše další kreslicí volání

Jaké jsou limity interoperability blokových anotací?

Blokové anotace jsou soukromá konvence, ne standardní funkce PDF. PDFlibBlock:Text a jeho sourozenci nejsou mezi typy anotací uvedenými v ISO 32000-1 §12.5.6.1, takže prohlížeč je bere jako neznámé anotace. Protože je příznak Invisible vypnutý a neexistuje žádný appearance stream, korektní prohlížeče pro ně nenakreslí nic, ale některé prohlížeče je mohou pořád vypisovat v panelu komentářů a další PDF nástroje ani nepochopí, ani nezachovají jejich sémantiku

  • Cokoli, co opouští váš systém, plňte s nastaveným bitem 1, aby výsledný soubor obsahoval jen obyčejný obsah stránky
  • PDF/A nepovoluje typy anotací mimo specifikaci PDF (ISO 19005-1 §6.5.2), takže archivovaný výstup nesmí nést nevyplněné blokové anotace
  • Bloky page-piece jsou v šabloně bezpečnější ponechat, protože /PieceInfo je standardní a konzumenti ignorují data, která nevlastní
  • Bloky nejsou formulářová pole: žádná interaktivita, žádná hodnota pole v souboru a žádný export do FDF nebo XFDF; pro data, která musí zůstat editovatelná, viz výměna formulářových dat FDF, XFDF a XFA v PDFlibPas

Pokud musí šablona deklarovat svá variabilní pole standardizovaným, validátorem kontrolovatelným způsobem pro tiskový workflow, bloky jsou špatný nástroj. Pro to je PDF/VCR-1 a manifest placeholderů šablon PDF/VCR-1 v PDFlibPas popisuje, jak tyto placeholdery žijí ve stromu struktury místo v anotacích

FAQ

Dokáže PDFlibPas plnit blokové šablony vytvořené jinými nástroji?

Ano, když nástroj ukládá bloky pod /PieceInfo /PDFlib /Blocks nebo /PieceInfo /PDFlib /Private /Blocks, ve formě slovníku nebo pole, s /Rect o čtyřech číslech. GetBlockCount je počítá po všech anotacích PDFlibBlock:* a vlastní klíče se vystavují přes GetBlockProperty

Mění FillTextBlock font, který jsem před voláním vybral?

Ne. FillTextBlock uloží vybraný font, velikost textu, barvu textu a zarovnání, aplikuje vlastnosti bloku fontname, fontsize, fillcolor a alignment a potom obnoví původní stav, i když vymístění selže

Jak umístím vektorové logo nebo stránku s čárovým kódem do bloku?

Načtěte zdrojové PDF, uložte SelectedDocument jako jeho ID, v případě potřeby znovu vyberte dokument šablony a zavolejte FillPDFBlock(Page, BlockIndex, SourceID, SourcePage, 1). Stránka se importuje jako Form XObject, takže zůstává vektorová a vymístí se do bloku se zachovaným poměrem stran, pokud nepřidáte bit 2

Blokové šablony, import stránek jako Form XObject a zbytek toolkitu pro variabilní data dodává PDFlibPas, losLab PDF Library for Delphi, který pokrývá tvorbu PDF, editaci a tiskovou produkci pro projekty v Delphi a C++Builderu