Technický článek

Vytváření PDF od nuly pomocí komponenty PDFium v Delphi

PDFium má pověst prohlížecího enginu, renderovacího jádra za záložkou PDF v prohlížeči Chrome, takže první věc, kterou je třeba si ujasnit, je to, že komponenta PDFium dokáže také sestavit dokument, který předtím nikdy neexistoval. Autorská strana obaluje rozhraní API stránkových objektů (page-object) z knihovny PDFium: vytvoříte prázdný dokument, přidáte stránky s přesnými rozměry a na každou stránku vložíte text, vektorové cesty a obrázky na souřadnice, které si zvolíte. Není zde žádný jazyk pro popis stránky, který byste se museli učit, a v celém procesu nefiguruje žádný tiskový ovladač. Voláte metody, knihovna sestaví objekty PDF a SaveAs výsledek serializuje

Co však nedostanete, je engine pro rozložení (layout engine). To je natolik důležité, abychom to řekli hned na začátek, protože to formuje každý níže uvedený příklad. Komponenta PDFium umístí obsah tam, kam jí řeknete, v absolutních souřadnicích a nikde jinde. Nezalámě odstavec, nenechá text přetéct přes konec stránky ani nespočítá tabulku z řádků a sloupců. To je váš úkol. Pokud jste přišli s očekáváním něčeho, co přeformátuje text jako textový procesor, upravte svá očekávání: jedná se o přesné nízkoúrovňové API pro umisťování, které má blíže ke kreslení na plátno než k sazbě dokumentu. Pro generované faktury, certifikáty, štítky a stránky s výkazy, kde už víte, kam každý prvek patří, je tato přesnost přesně to, co chcete

Minimum, které vygeneruje soubor

Tři volání stojí mezi prázdným TPdf a uloženým PDF: vytvořit dokument, přidat stránku, zapsat ho. Všechno ostatní je obsah, který navrstvíte mezi to

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 plete lidi, kteří viděli starší úryvky kódu: po volání CreateDocument nepřiřazujete Pdf.Active := True. Vlastnost Active oznamuje, zda existuje handle dokumentu, a CreateDocument ho již vytvořilo, takže tato vlastnost je True v okamžiku, kdy se volání vrátí. Její opětovné nastavení je v nejlepším případě zbytečná operace a v nejhorším případě zavádějící pro dalšího čtenáře. Active si zaslouží své místo na konci: přiřazení False uvolní podkladový dokument před voláním Free, což je čistý způsob ukončení. K CreateDocument a načítání souboru otevřením přistupujte jako ke vzájemně se vylučujícím operacím. Knihovna odmítne vytvořit nový dokument v objektu TPdf, který již má jeden dokument otevřený, takže opětovné použití znamená, že musíte nejprve zavřít aktuální dokument

Souřadnice začínají vlevo dole

Druhá dvojice argumentů u AddText a u každého volání pro umístění představuje bod v uživatelském prostoru PDF. Počátek leží v levém dolním rohu stránky, X směřuje doprava a Y směřuje nahoru. Jedna jednotka je jeden bod, 1/72 palce, takže stránka formátu A4 má rozměry 595 x 842 jednotek a formát US Letter 612 x 792. Toto Y směřující nahoru je zdaleka nejčastějším zdrojem zmatení typu „můj text je mimo stránku“, protože souřadnice obrazovky a bitmapy umisťují počátek nahoru, přičemž Y roste směrem dolů. Na stránce vysoké 842 bodů bude nadpis blízko horního okraje ležet zhruba na pozici Y 780, nikoli Y 60. Když text skončí někde, kde jste to nečekali, výška stránky minus vaše Y je téměř vždy číslo, které jste ve skutečnosti měli na mysli

Prvním argumentem pro AddPage je pozice pro vložení. Udává se od jedničky, přičemž 0 funguje jako pohodlná zkratka pro „začátek dokumentu“. Předáte-li 0 nebo 1 pro první stránku, bude stránka vložena na začátek; předáte-li hodnotu odpovídající počtu stránek, ke kterým přidáváte, stránka se vloží na konec. Nově přidaná stránka se také stane aktuální stránkou, na kterou cílí následná volání pro kreslení, takže po jejím přidání neexistuje žádný samostatný krok typu „vybrat tuto stránku“. Pokud přidáte několik stránek a později potřebujete kreslit zpět na dřívější stránku, nastavte PageNumber pro posunutí kurzoru; dokud plníte stránky v pořadí, v jakém je vytváříte, můžete to nechat být

Psaní textu a pravidlo pro písma, které tiše kousne

Signatura funkce AddText obsahuje vše, co jedna dávka textu potřebuje: řetězec, název písma, velikost v bodech, kotvící bod X a Y a dále volitelnou barvu, alfa bajt pro průhlednost a úhel otočení ve stupních

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;

Alfa bajt nabývá hodnot od $00 (neviditelný) do $FF (neprůhledný), což je to, co dělá z razítka konceptu vodoznak místo plného bloku: $30 je zhruba devatenáctiprocentní neprůhlednost, dostatečná k tomu, aby se přes ni dalo číst. Úhel otáčí textem proti směru hodinových ručiček kolem jeho kotevního bodu, takže 45 stupňů vám dá klasické razítko z rohu do rohu. Nic z toho nevyžaduje samostatnou funkci vodoznaku. Vodoznak je jen velké, poloprůhledné, otočené volání AddText a to, zda ho vykreslíte před textem nebo po něm, rozhoduje o tom, zda bude umístěn za obsahem nebo na něm

Písma (fonty) si zaslouží pečlivé prozkoumání, protože režim selhání je tichý. Když předáte název písma, komponenta PDFium požádá operační systém o data TrueType pro toto písmo a vloží jej do dokumentu. To je důvod, proč se soubor vytvořený na vašem počítači vykreslí naprosto identicky i na systému, kde dané písmo nikdy nebylo nainstalováno. Záludnost spočívá v tom, co se stane, když se název nepodaří přeložit: překlep nebo rodina písem, která na stroji sestavujícím dokument prostě chybí. Nevyvolá se žádná výjimka. Knihovna se vrátí k vytvoření textového objektu, který nenese nic vloženého a má název písma pouze jako popisek, a nechá na prohlížeči, aby ho nahradil něčím, co považuje za podobné. Text se ve vašich testech zobrazí, vypadá věrohodně, avšak jeho metrika nebo samotné znaky se posunou v okamžiku, kdy soubor otevřete někde s nainstalovanými jinými písmy. Používejte názvy, o kterých víte, že na stroji generujícím dokumenty existují, považujte seznam písem za závislost nutnou pro nasazení a než budete výstupu důvěřovat, otevřete si ukázku v prohlížeči na čistém systému

Vektorové tvary: vytvořte cestu a pak ji potvrďte

Čáry, obdélníky a vyplněné oblasti se kreslí pomocí cesty. Založíte ji pomocí funkce CreatePath, která nastaví počáteční bod a veškeré stylování najednou – režim výplně, barvu výplně a tahu včetně příslušných alfa bajtů, tloušťku tahu a vzhled zakončení a spojení čar. Pak ji rozšíříte voláním LineTo, BezierTo a ClosePath. Nakonec funkci AddPath dokončenou cestu zanese na stránku. Na krok potvrzení se snadno zapomene a pokud ho přeskočíte, nic se 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;

Dvě přetížení (overloads) pokrývají běžné případy. Verze se čtyřmi souřadnicemi bere X, Y, šířku a výšku a v jediném volání vám poskytne obdélník zarovnaný s osami, po kterém sáhnete, když chcete nakreslit čáru, okraj buňky nebo vyplněný panel na pozadí. Varianta se dvěma souřadnicemi nastaví pouze výchozí bod a vy sami si vykreslíte zbytek obrysu pomocí volání LineTo a BezierTo. Režim výplně (fill mode) řídí, jak se prolínající se oblasti vymalují: fmWinding (nonzero winding) vyhovuje většině plných tvarů, fmAlternate (even-odd) si poradí s výřezy a samoprotínajícími se obrysy a fmNone vytvoří cestu pouze pro tahy bez jakékoliv výplně, což je přesně to, co používá dělící čára (divider) výše

Tabulky jsou tvořeny cestami a textem složenými ručně

Protože neexistuje žádný primitivní prvek pro tabulky, tabulka je v podstatě cyklus. Určíte si posuny sloupců v ose X a výšku řádku, každou buňku zapíšete pomocí AddText a nakreslíte čáry jako pravoúhlé cesty. Matematika je sice na vás, ale je to jednoduché, a jakmile ji napíšete, dá se to zobecnit na libovolnou mřížku, kterou budete potřebovat

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šimněte si posouvání souřadnice Y dolů o výšku řádku v každém kroku – opět z toho důvodu, že směr nahoru je pozitivní. Zde se také projevuje absence měření textu: nic nebrání dlouhému názvu položky v přesahu do sousedního sloupce, protože knihovna netuší, jak široký se váš řetězec vyrendroval. Pro výstup s pevným formátem, kde řídíte data, sloupce dimenzujete velkoryse a můžete se posunout dál. Pokud jde o skutečně proměnlivý obsah, musíte vstupy buď omezit, nebo před jejich umístěním změřit šířku znaků. To je okamžik, kdy se začne vyplácet dedikovaná kompoziční knihovna

Obrázky a vícestránkové dokumenty

Rastrový obsah se vkládá pomocí pomocných funkcí pro obrázky. AddPicture převezme načtený objekt TPicture a umístí jej do daného bodu s volitelnou šířkou a výškou pro nastavení měřítka. Funkce AddImage přijímá přímo cestu k souboru nebo objekt TBitmap a AddJpegImage proudově odesílá bajty JPEG souboru, aniž by je zbytečně převáděla přes formát bitmapy. Stejně jako u všeho ostatního, souřadnice pro umístění udávají levý dolní roh obrázku v uživatelském prostoru a parametry jako šířka a výška udávají velikost přímo na stránce v bodech, nikoliv velikost zdroje v pixelech

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;

Vícestránkový dokument je de facto vzor jedné stránky uzavřený v cyklu. Každé zavolání AddPage přidá novou stránku a nastaví ji jako aktuální, takže obsah stránky a zápatí, které dále vykreslíte, skončí právě na té stránce, kterou jste právě přidali. Neupravujete vlastnost PageNumber (číslo stránky) uvnitř této smyčky, protože přidáním stránky se na ni přesunul už kurzor. PageNumber budete potřebovat pouze tehdy, když se chcete vrátit na některou stránku mimo pořadí jejich tvorby. Funkci SaveAs volejte pouze jednou až na úplném konci, když je naplněna i poslední ze stránek. Jestliže nepotřebujete obyčejný soubor, nýbrž archivační formát, tentýž objekt dokumentu poskytuje metodu SaveAsPdfA a další varianty splňující požadavky, tudíž vaše volba výstupní normy se odrazí v jiném volání pro uložení, ale proces generování zůstává týž

Kam se to hodí

Poctivě řečeno, autorské rozhraní (authoring API) komponenty PDFium je jen tenkou, věrnou vrstvou překrývající stránkový model (page-object model) jádra PDFium. Umožňuje skutečnou tvorbu dokumentů, opravdové vkládání písem, zpracování skutečného vektorového a rastrového obsahu a jejich serializaci do podoby souboru, který odpovídá standardům. Nejedná se ovšem o nástroj pro tvorbu přeformátovávaných textů s automatickým zalamováním, a ani se o to nesnaží. Zásadní hranice mezi nimi spočívá v rozvržení textu (layoutu). Jestliže je váš výstup v podstatě šablona – ať se již jedná o faktury, osvědčení, štítky nebo dashboardy usazené do pevně vymezené mřížky – pak vám tento model pevných souřadnic nabídne přímou, rychlou cestu, u které kód zůstává zcela čitelným. Budete-li však pracovat s delšími textovými útvary, u kterých je třeba, aby se samy přizpůsobily okraji stránky a uměly správně stránkovat, v podstatě na tato volání jen naroubujete vlastní kompoziční systém, k čemuž ale tento nástroj není určen. Vědět přesně, na jaké straně této pomyslné hranice stojíte, tvoří samotný základ pro to správné rozhodnutí

Zde popsané metody pro tvorbu představují součást komponenty PDFium Component pro jazyk Delphi. Ta propojuje tuto tvůrčí rovinu s funkcemi zaměřenými na renderování a extrahování textu, kvůli nimž je samotný systém PDFium mnohem proslulejší