Technical Article

Vytváranie PDF dokumentov od nuly pomocou PDFium VCL v Delphi

PDFium má povesť prehliadacieho jadra, teda vykresľovača, ktorý stojí za záložkou PDF v prehliadači Chrome. Prvou vecou, ktorú je potrebné objasniť, je, že PDFium VCL dokáže vytvoriť aj dokument, ktorý nikdy predtým neexistoval. Strana pre vytváranie dokumentov obaľuje API objektov stránok PDFium: vytvoríte prázdny dokument, pridáte stránky s explicitnými rozmermi a na každú stránku umiestnite text, vektorové cesty a obrázky na súradnice, ktoré si zvolíte. Nemusíte sa učiť žiadny jazyk na popis stránky a v celom procese nefiguruje žiadny tlačový ovládač. Voláte metódy, knižnica zostavuje objekty PDF a metóda SaveAs serializuje výsledok.

Čo však nezískate, je rozvrhovacie jadro (layout engine). Toto je dôležité povedať hneď na úvod, pretože to formuje každý príklad nižšie. PDFium VCL umiestňuje obsah presne tam, kam mu prikážete, v absolútnych súradniciach, a nikde inde. Nebude zalamovať odsek, presúvať text pri zlome stránky ani počítať tabuľku z riadkov a stĺpcov. Tieto úlohy sú na vás. Ak očakávate niečo, čo automaticky zalamuje prózu ako textový procesor, upravte svoje očakávania: toto je presne, nízkoúrovňové pozičné API, ktoré má bližšie ku kresleniu na plátno než k sadzbe dokumentu. Pre generované faktúry, certifikáty, štítky a stránky správ, kde už vopred viete, kam každý prvok patrí, je táto presnosť presne tým, čo chcete.

Minimum potrebné na vytvorenie súboru

Medzi prázdnym objektom TPdf a uloženým súborom PDF stoja tri volania: vytvoriť dokument, pradať stránku a zapísať ju. Všetko ostatné je obsah, ktorý vrstvíte medzi tieto kroky.

uses
  Vcl.Graphics,   // for clBlack and TColor
  PDFium;         // TPdf lives here

procedure CreateBlankPdf(const FileName: string);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                 // empty in-memory document
    Pdf.AddPage(0, 595, 842);           // A4 portrait, in points
    Pdf.AddText('First page', 'Arial', 18, 50, 780);
    Pdf.SaveAs(FileName);               // serialize to disk
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Jeden detail môže pomiasť tých, ktorí videli staršie ukážky kódu: po volaní CreateDocument nepriraďujete vlastnosť Pdf.Active := True. Vlastnosť Active hlási, či existuje popisovač (handle) dokumentu, a CreateDocument ho už vytvoril, takže táto vlastnosť má hodnotu True hneď po návrate z tohto volania. Jej opätovné nastavenie je prinajlepšom zbytočná operácia a prinajhoršom zavádzajúca pre ďalšieho čitateľa kódu. Svoju úlohu plní vlastnosť Active pri ukončení: priradenie hodnoty False uvoľní podkladový dokument pred zavolaním Free, čo predstavuje čisté poradie pri upratovaní pamäte. Považujte CreateDocument a otvorenie načítaním súboru za vzájomne sa vylučujúce operácie. Knižnica odmietne vytvoriť nový dokument na objekte TPdf, ktorý už má nejaký dokument otvorený, takže opätovné použitie znamená najprv zatvorenie aktuálneho dokumentu.

Súradnice začínajú v ľavom dolnom rohu

Druhý pár argumentov metódy AddText, a vlastne každého volania umiestnenia, predstavuje bod v používateľskom priestore PDF. Počiatok súradníc leží v ľavom dolnom rohu stránky, os X smeruje doprava a os Y smeruje nahor. Jednou jednotkou je jeden bod, teda 1/72 palca, takže stránka A4 má rozmery 595 × 842 jednotiek a formát US Letter má 612 × 792. Smer osi Y nahor je najčastejším zdrojom nedorozumení, kedy text končí mimo stránky, pretože súradnice obrazovky a bitmapy umiestňujú počiatok hore a os Y smeruje nadol. Na stránke vysokej 842 bodov sa nadpis blízko horného okraja nachádza okolo Y 780, nie Y 60. Keď prvok skončí na neočakávanom mieste, výška stránky mínus vaša hodnota Y je takmer vždy to číslo, ktoré ste v skutočnosti chceli zadať.

Metóda AddPage berie ako svoj prvý argument pozíciu vloženia, vyjadrenú od jednotky, pričom 0 slúži ako pohodlná skratka pre „začiatok dokumentu“. Zadajte 0 alebo 1 pre prvú stránku a stránka sa vloží na začiatok; zadajte hodnotu zodpovedajúcu počtu stránok, ak ich chcete pripojiť na koniec. Novo pridaná stránka sa stáva aj aktuálnou stránkou, na ktorú smerujú nasledujúce volania kreslenia, takže po jej pridaní nie je potrebný žiadny samostatný krok na výber stránky. Ak pridáte niekoľko stránok a neskôr potrebujete kresliť na niektorú z predchádzajúcich, nastavte vlastnosť PageNumber na presun ukazovateľa; kým plníte stránky v poradí, v akom ich vytvárate, môžete to nechať tak.

Zápis textu a pravidlo o písmach, ktoré ticho zlyháva

Deklarácia metódy AddText nesie všetko, čo jeden textový úsek potrebuje: reťazec, názov písma, veľkosť v bodoch, kotvu X a Y, potom voliteľnú farbu, bajt alfa kanálu pre priehľadnosť a uhol otočenia v stupňoch.

procedure WriteHeader(Pdf: TPdf; const Title, Author: string);
begin
  // Title in black, default opacity, no rotation
  Pdf.AddText(Title, 'Arial', 20, 50, 780);
  // A lighter byline 24 points below it
  Pdf.AddText('By ' + Author, 'Arial', 11, 50, 756, clGray);
  // A faint diagonal draft stamp across the page
  Pdf.AddText('DRAFT', 'Arial', 64, 180, 380, clGray, $30, 45.0);
end;

Bajt alfa kanálu nadobúda hodnoty od $00 (neviditeľný) po $FF (nepriehľadný), čo robí z nápisu DRAFT vodoznak a nie plný blok: hodnota $30 predstavuje zhruba devätnásťpercentnú nepriehľadnosť, čo stačí na to, aby sa cez ňu dal čítať text. Uhol otočí text proti smeru hodinových ručičiek okolo jeho kotvy, takže 45 stupňov vytvorí klasickú šikmú pečiatku. Na toto nepotrebujete žiadnu špeciálnu funkciu vodoznaku. Vodoznak je len veľké, polopriehľadné a otočené volanie AddText, pričom jeho vykreslenie pred alebo po tele dokumentu rozhoduje o tom, či bude ležať pod obsahom alebo nad ním.

Písma si zaslúžia podrobnejšie vysvetlenie, pretože spôsob ich zlyhania je tichý. Keď odovzdáte názov písma, PDFium VCL požiada operačný systém o dáta TrueType tohto písma a vloží ich do dokumentu, čo je dôvod, prečo sa súbor vytvorený na vašom počítači vykreslí rovnako aj na stroji, ktorý toto písmo nikdy nemal nainštalované. Problém nastáva, ak sa názov nepodarí nájsť: napríklad pri preklepe alebo ak písmo na zostavovacom počítači chýba. Nevyvolá sa žiadna výnimka. Knižnica namiesto toho vytvorí textový objekt, ktorý nesie názov len ako štítok, bez vložených dát, a ponechá na prehliadači, aby ho nahradil niečím podobným. Text sa v testoch zobrazí, vyzerá dôveryhodne, ale zmení metriky alebo znaky v momente, keď sa súbor otvorí na systéme s inými nainštalovanými písmami. Používajte názvy, o ktorých viete, že sú na generujúcom systéme prítomné, považujte zoznam písiem za závislosť nasadenia a otvorte vzorku v prehliadači na čistom systéme pred tým, než začnete výstupu dôverovať.

Vektorové tvary: zostavenie cesty a jej potvrdenie

Čiary, obdĺžniky a vyplnené oblasti sa vytvárajú pomocou cesty (path). Cestu otvoríte metódou CreatePath, ktorá nastaví počiatočný bod a všetky štýly naraz: režim výplne, farbu výplne a ťahu s ich vlastnými alfa kanálmi a šírku ťahu. Potom ju rozšírite pomocou LineTo, BezierTo a ClosePath, a nakoniec metóda AddPath potvrdí hotovú cestu na stránku. Krok potvrdenia cesty sa dá ľahko vynechať, pričom v takom prípade sa nič nevykreslí.

procedure DrawDivider(Pdf: TPdf; X, Y, Width: Single);
begin
  // A thin horizontal rule. The rectangle overload sets a box directly:
  // X, Y, Width, Height, then fill mode and colors.
  Pdf.CreatePath(X, Y, Width, 0.5, fmNone, clBlack, $FF,
    True, clBlack, $FF, 1.0);
  Pdf.AddPath;
end;

procedure DrawTriangle(Pdf: TPdf);
begin
  // Point overload: start at the first vertex, line to the rest, close.
  Pdf.CreatePath(200, 300, fmWinding, clBlue, $80, True, clNavy, $FF, 2.0);
  Pdf.LineTo(300, 300);
  Pdf.LineTo(250, 400);
  Pdf.ClosePath;
  Pdf.AddPath;          // nothing is drawn until this runs
end;

Dve preťaženia pokrývajú bežné prípady. Štvor-súradnicová forma berie X, Y, šírku a výšku a poskytuje obdĺžnik zarovnaný s osami v jednom volaní, čo využijete na nakreslenie deliacej čiary, okraja bunky alebo vyplneného panelu pozadia. Dvoj-súradnicová forma nastavuje iba počiatočný bod a zvyšok obrysu vykresľujete sami pomocou LineTo a BezierTo. Režim výplne určuje, ako sa vykreslia prekrývajúce sa oblasti: fmWinding (nenulové navíjanie) vyhovuje väčšine pevných tvarov, fmAlternate (párne-nepárne) spracováva výrezy a pretínajúce sa obrysy a fmNone ponechá iba obrys bez výplne, čo je prípad vyššie uvedeného deliaceho riadku.

Tabuľky sú cesty a text, zostavené ručne

Keďže neexistuje žiadny tabuľkový primitív, tabuľka sa rieši cyklom. Určíte odsadenia stĺpcov X a výšku riadku, zapíšete každú bunku pomocou AddText a nakreslíte čiary pomocou obdĺžnikových ciest. Výpočty sú na vás, sú však priamočiare a raz napísaný kód sa dá zovšeobecniť na akúkoľvek mriežku, ktorú potrebujete.

procedure DrawTable(Pdf: TPdf; Left, Top: Double);
const
  ColX: array[0..2] of Double = (0, 110, 210);  // column offsets
  RowH = 20;
var
  Y: Double;
  Row: Integer;
begin
  // Header row
  Pdf.AddText('Item', 'Arial', 10, Left + ColX[0], Top);
  Pdf.AddText('Qty', 'Arial', 10, Left + ColX[1], Top);
  Pdf.AddText('Price', 'Arial', 10, Left + ColX[2], Top);

  // Rule under the header
  Pdf.CreatePath(Left, Top - 5, 260, 0.5, fmNone, clBlack, $FF);
  Pdf.AddPath;

  // Data rows, stepping Y downward each iteration
  Y := Top;
  for Row := 1 to 3 do
  begin
    Y := Y - RowH;
    Pdf.AddText('Item ' + IntToStr(Row), 'Arial', 9, Left + ColX[0], Y);
    Pdf.AddText(IntToStr(Row * 2), 'Arial', 9, Left + ColX[1], Y);
    Pdf.AddText('$' + IntToStr(Row * 10) + '.00', 'Arial', 9, Left + ColX[2], Y);
  end;
end;

Všimnite si posun osi Y smerom nadol o výšku riadku pri každom prechode, opäť preto, že smer nahor je kladný. Tu sa prejavuje aj chýbajúce meranie textu: nič nebráni tomu, aby dlhý názov položky pretiekol do susedného stĺpca, pretože knižnica nevie, aký široký bol vykreslený reťazec. Pri pevných formátoch výstupu, kde máte dáta pod kontrolou, dimenzujete stĺpce dostatočne veľké a pokračujete. Pri premenlivom obsahu musíte buď obmedziť vstupy, alebo sami zmerať šírku glyfov pred ich umiestnením. To je moment, kedy sa začína vyplácať použitie špecializovanej knižnice pre sadzbu.

Obrázky a viacero stránok

Rastrový obsah sa vkladá pomocou pomocných metód pre obrázky. AddPicture berie načítaný objekt TPicture a umiestňuje ho na zadaný bod s voliteľnou šírkou a výškou pre zmenu mierky; AddImage prijíma cestu k súboru alebo priamo TBitmap a metóda AddJpegImage prenáša bajty JPEG bez nutnosti prevodu na bitmapu. Ako pri všetkom ostatnom, súradnice umiestnenia predstavujú ľavý dolný roh obrázka v používateľskom priestore a šírka a výška sú rozmery na stránke v bodoch, nie rozmery zdrojového obrázka v pixeloch.

procedure CreateMultiPageReport(const FileName: string; PageCount: Integer);
var
  Pdf: TPdf;
  P: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    for P := 1 to PageCount do
    begin
      Pdf.AddPage(P, 595, 842);     // append; the new page becomes current
      Pdf.AddText('Page ' + IntToStr(P) + ' of ' + IntToStr(PageCount),
        'Arial', 10, 50, 30);       // footer near the bottom edge
      // ... draw this page's body here ...
    end;
    Pdf.SaveAs(FileName);
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Viacstránkový dokument je v podstate len vzor pre jednu stránku spustený v cykle. Každá metóda AddPage pripojí stránku a urobí ju aktuálnou, takže telo a päta, ktoré vykreslíte potom, skončia na stránke, ktorú ste práve pridali. V tomto cykle už nepriraďujete vlastnosť PageNumber, pretože pridanie stránky tam ukazovateľ posunulo samo. Vlastnosť PageNumber potrebujete iba vtedy, ak sa vraciate na stránku mimo poradia jej vytvárania. Metódu SaveAs zavolajte iba raz na konci, po naplnení poslednej stránky. Ak potrebujete archívny profil namiesto bežného súboru, rovnaký objekt dokumentu sprístupňuje metódu SaveAsPdfA a ďalšie varianty zhody, takže výber výstupného štandardu je otázkou iného volania pri ukladaní, nie iného postupu pri zostavovaní.

Kam toto riešenie patrí

Úprimne povedané, API pre vytváranie dokumentov v PDFium VCL je vernou a tenkou vrstvou nad modelom objektov stránok PDFium: skutočná tvorba dokumentov, reálne vložené písma, plnohodnotný vektorový a rastrový obsah serializovaný do súboru spĺňajúceho štandardy. Nie je to však a ani sa netvári ako dokumentové jadro so samovoľným zalamovaním. Deliacou čiarou je rozloženie textu. Ak je váš výstup šablónovitý (faktúry, certifikáty, štítky, prehľady vykresľované do pevnej mriežky), model absolútnych súradníc je priamy, rýchly a kód zostáva čitateľný. Ak je však vaším výstupom dlhý text, ktorý sa musí sám zalamovať a stránkovať, museli by ste nad týmito volaniami vybudovať vlastné rozvrhovacie jadro, čo by bolo nesprávnym nástrojom pre danú úlohu. Uvedomenie si toho, na ktorej strane tejto línie stojíte, predstavuje väčšinu rozhodnutia.

Metódy pre vytváranie dokumentov popísané v tomto článku sú súčasťou komponentu PDFium VCL Component pre Delphi, ktorý spája túto tvorivú cestu s funkciami vykresľovania a extrakcie textu, ktorými je PDFium známejšie.