Technický článek

Reporty v PDF v Delphi s HotPDF: TextOut, písma a obrázky

Generování reportu se v jádru scvrkne na umístění tří věcí na stránku a jejich shodu na tom, kde vlastně sedí: text na známých souřadnicích, fonty, které se vykreslují stejně na serveru jako na vašem desktopu, a obrázky s velikostí, která se vejde. Všechno ostatní, co knihovna pro reporty dělá, je uspořádané kolem těchto tří věcí. HotPDF, generovací knihovna PDF od losLab pro Delphi a C++Builder, vám dává každou z nich jako přímé volání na objektu stránky, a jediné skutečné tření je souřadnicový systém pod tím vším, který běží opačným směrem než plátno VCL, na které jste zvyklí. Vyřešte si nejdřív tuto orientaci a zbytek práce s rozvržením s vámi přestane bojovat

Umístění textu a počátek v levém dolním rohu

Skoro každému vyjde první report vzhůru nohama. Titulek přistane blízko dolního okraje a každý další řádek pod ním šplhá nahoru. Nic přitom nefunguje špatně. Uživatelský prostor PDF, definovaný v ISO 32000-1 §8.3, klade počátek do levého dolního rohu s Y rostoucím nahoru, což je zrcadlový obraz plátna GDI, kde Y roste dolů od levého horního rohu. Pět minut strávených smířením se s tím ušetří rozvržení, které byste jinak přepisovali ve chvíli, kdy čísla přestanou dávat smysl

Diagram HotPDF kontrastující počátek souřadnic VCL vlevo nahoře s počátkem PDF vlevo dole, kde TextOut umístí titul 50 bodů od horního okraje strany Letter na Y 792 minus 50
PDF user space zrcadlí canvas VCL, takže titul 50pt od horního okraje stránky Letter je TextOut(50, 792 - 50, 0, 'INVOICE') a tatáž konverze drží každou souřadnici reportu intuitivní

Ústřední volání objektu stránky je TextOut(X, Y, Angle, Text). X a Y umísťují text v bodech od levého dolního rohu a Angle jej otáčí ve stupních, čímž se bez jakékoli speciální podpory nakreslí diagonální razítko DRAFT nebo COPY. Trik, který nechá intuici vycvičenou na VCL dál fungovat, je vyjádřit Y jako výšku stránky minus vzdálenost, kterou chcete od horního okraje:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt od horního okraje Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // otočené razítko
    Pdf.AddPage;                                                // CurrentPage teď ukazuje sem
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // stav fontu se nepřenáší
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dvě stavová chování v tomto výpisu jsou zodpovědná za většinu chyb, které se projeví až na druhé stránce. AddPage přesměruje CurrentPage na stránku, kterou právě vytvořil, takže reference na stránku, kterou jste si dřív uložili do proměnné, už nekreslí tam, kde čekáte. Výběr fontu je také per stránka, ne per dokument. Přeskočíte-li SetFont po AddPage, první TextOut na čerstvé stránce spadne zpět na cokoli, s čím stránka začínala jako výchozí, ne na tučný font nadpisu, který jste nastavili o tři stránky dřív. Bezpečný návyk je zacházet s „začni novou stránku" a „znovu nastav stav textu" jako s jedním nedělitelným krokem ve smyčce reportu

Fonty, které existují na serveru, ne jen na vašem desktopu

Většina problémů s fonty jsou ve skutečnosti problémy s nasazením v přestrojení. Váš vývojový počítač má firemní font nainstalovaný, takže report na vaší obrazovce vypadá správně a jde do produkce. Produkční hostitel spouští úlohu pod servisním účtem, který tento font nikdy neměl nainstalovaný, renderer potichu nahradí čímkoli, co najde, a první, co se o tom kdo dozví, je zákazník ptající se, proč se změnila hlavička dopisu. Cesta ven je přestat důvěřovat adresáři fontů operačního systému a načíst font ze souboru, který na disk umístí váš instalátor. Registrační volání pro Unicode v HotPDF přijímá cestu a dělá přesně tohle:

Diagram problému nasazení fontů PDF v Delphi: produkční server potichu substituuje chybějící font, zatímco RegisterUnicodeTTF načte TTF z nasazeného souboru a vloží jej do PDF
Spoléhání na složku písem OS zlomí, když produkční servisní účet písmo postrádá, zatímco načtení TTF z nasazeného souboru vloží glyfy a každý hostitel vykreslí identicky
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

TextOut přijímá WideString přímo, což je důležitější, než se na první pohled zdá. Jméno zákazníka s diakritikou, německá ulice, polské město: to nejsou okrajové případy, jsou to běžný obsah zákaznické tabulky, a projdou stejným voláním jako ASCII popisky, které máte napevno v kódu, pokud registrovaný font skutečně obsahuje potřebné glyfy. S vloženými fonty jede jedno omezení verze: dokument musí být PDF 1.5 nebo novější, takže pokud vás nesouvisející požadavek drží u starší verze, přesně tohle vám potichu selže. Písma zprava doleva, jako arabština a hebrejština, potřebují skutečné tvarování (shaping), ne přímé vyhledání glyfu, a to má vlastní pipeline; viz náš článek o tvarování textu ve složitých písmech v HotPDF

Když žádný nainstalovaný font nedokáže vyjádřit, co potřebujete, představte si znaky MICR na šeku nebo proprietární sadu symbolů, fonty Type 3 tuto mezeru zaplní. Každý glyf definujete jako malý content stream přes RegisterType3Font a AddType3Glyph. Je to specializovaný koutek API a sáhnete po něm zřídka, ale je to o dost čistší než rozházet po stránce stovky drobných bitmap symbolů

Obrázky: prostřední argumenty jsou šířka a výška, ne roh

Práce s obrázky se dělí na dva kroky a udržet je oddělené je celá podstata. AddImage přijme TBitmap nebo TJPEGImage, jednou jej vloží a vrátí index. Grafika PNG se před tím musí dekódovat na bitmapu. ShowImage pak tento index vykreslí kamkoli a tolikrát, kolikrát chcete. Pořadí argumentů u ShowImage je jediné místo, kde se vyplatí zpomalit a číst pořádně:

Diagram image pipeline HotPDF, kde AddImage vloží bitmapu jednou a vrátí index, ShowImage ji umístí podle šířky a výšky a pořadí argumentů není dvojice rohů
AddImage vloží pixely jednou a každé volání ShowImage znovu použije ten index; střední argumenty jsou width a height, nikoli souřadnice protilehlého rohu
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // dekóduje PNG na bitmapu
    LogoIdx := Pdf.AddImage(Logo, icFlate); // bezztrátové pro grafiku s plochými barvami
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): ne (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Dvě čísla za pozicí jsou šířka a výška. Nejsou to souřadnice protilehlého rohu, a poslední argument je úhel otočení ve stupních. Přečtěte si signaturu jako box X1/Y1/X2/Y2 a logo 120 na 40 umístěné na (50, 700) se místo toho natáhne odtamtud až k (120, 40), rozvalí se přes většinu stránky. Výstup dělá tuto chybu zjevnou, zatímco zdrojový kód vypadá naprosto rozumně, což je to, co promarní celé odpoledne. KeepImageAspectRatio má výchozí hodnotu True, takže box se špatným poměrem stran obrázek zarovná s okraji (letterbox) místo jeho zdeformování; přepněte na False jen tehdy, když opravdu chcete roztáhnout

Rozdělení na registraci a umístění se vyplácí u dlouhých běhů. Protože AddImage vloží pixely jednou a každé ShowImage s daným indexem ukazuje zpět na stejný vložený objekt, o velikosti souboru rozhoduje to, kde AddImage zavoláte. Zavolejte jej uvnitř smyčky stránek u 500stránkového výpisu a stejné logo se vloží 500krát. Zavolejte jej jednou před smyčkou, uchovejte si index, a logo se uloží jen jednou. Malý slovník klíčovaný cestou k assetu stačí na to, aby se každý odlišný obrázek registroval přesně jednou

Volba kodeku je druhá páka pro velikost. Fotografický obsah, naskenované přílohy a podobně patří do JPEG: předejte AddImage hodnotu icJpeg a snižte JpegQuality na zhruba 85, protože vlastnost začíná na 100 a rozdíl při 85 je na vytištěné stránce neviditelný. Grafika s plochými barvami jako loga, grafy a čárové kresby patří do icFlate, kde je bezztrátová komprese už kompaktní a JPEG by kolem ostrých hran rozmazal viditelné vlnění. Běh výpisů, který na každou stránku tlačí jednu fotografii v plné kvalitě, může nafouknout velikost na gigabajty; stejný obsah při JPEG 85 přistane zhruba na desetině velikosti a žádný čtenář to nepozná

Linky, boxy a stínování pomocí primitiv cesty

Vodorovná čára pod hlavičkou tabulky a šedý box za souhrnným číslem nemusí být obrázky. Nakreslete je jako vektory a zůstanou ostré při jakémkoli přiblížení, tisknou se ostře a do souboru přidají takřka nic. HotPDF se drží stejného modelu, jaký používají surové content streamy PDF: sestavíte cestu a pak zavoláte operátor, který ji vykreslí

// Vodorovná linka pod hlavičkou tabulky
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Stínovaný box se součtem: X, Y, šířka, výška
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

Pořadí není volitelné: nastavte stav vykreslování, sestavte cestu a pak zavolejte Stroke nebo Fill. Cesta, kterou sestavíte, ale nikdy nevykreslíte, na stránku nepřidá nic, což je téměř vždy odpověď na to, proč se linka „neobjevuje". SetRGBFillColor přijímá jediné TColor, takže se do ní bez potíží vejdou známé konstanty VCL jako clNavy a clBlack, a Rectangle používá stejné argumenty šířka a výška jako umístění obrázku, ne dva rohy. Jedno varování k tenkým linkám: cokoli pod zhruba půl bodu může na monitoru vypadat elegantně a pak zmizet na kancelářské tiskárně s 600 dpi, takže 0,75pt je rozumné minimum pro jakoukoli linku, která má přežít tisk

Stránkování proti skutečným datům, ne ukázkovým

Jeden detail, který je potřeba vyřešit dřív, než rozvržení zatuhne: číselné sloupce by měly být zarovnané na pravém okraji, a způsob, jak toho dosáhnout, je změřit vykreslenou šířku každé hodnoty a umístit ji zpětně od hranice sloupce, ne doplňovat řetězec mezerami zleva. Vyplňování mezerami se srovná jen v monospaced fontu, a nikdo finanční report nesází monospaced fontem. Nejdřív protáhněte hodnoty přes rutiny Delphi citlivé na locale, jako je FormatFloat, aby oddělovač tisíců, jehož šířku měříte, byl přesně ten, který zákazníkovo locale skutečně zobrazí

Nebezpečí u stránkování je v tom, že jej napíšete proti demo datové sadě, kde se deset krátkých řádků vejde na jednu stránku a smyčka se nikdy nemusí zlomit. Produkce vám hodí na stůl zákazníka, jehož název firmy má 140 znaků, a výpis se 4 000 položkami, a teď se smyčka musí lámat správně pokaždé. Vzor, který obstojí, je jediný kurzor Y, který se posouvá dolů, jak odečítáte výšku každého řádku, a kontrola, která začne novou stránku ve chvíli, kdy by kurzor překročil dolní okraj. „Dolů" tady znamená klesající Y, což je to jediné místo, kde počátek v levém dolním rohu zůstává neintuitivní. Držte tohle všechno v jedné rutině, která na nové stránce také znovu vydá SetFont a překreslí běžící hlavičku, a chyby posunuté o jednu stránku nikdy nezískají oporu. Když stejné reporty musí zároveň splňovat archivní nebo přístupnostní pravidla, volby, které uděláte přímo tady, jaké fonty vložíte, zda je výstup otagovaný, jaké barevné prostory použijete, jsou přesně to, co tyto standardy hlídají; průvodce HotPDF PDF/A, PDF/X a PDF/UA stojí za přečtení dřív, než šablona ztvrdne

Každé zde ukázané volání, umístění textu, registrace fontů, vkládání obrázků i kreslení cest, je součástí HotPDF Delphi Component pro Delphi a C++Builder, jehož referenční dokumentace popisuje celé výstupní API vedle funkcí pro formuláře, šifrování a podepisování, se kterými sousedí