HotPDF vykresluje vektorovou grafiku tím, že sestavuje cestu na aktuální stránce a poté požádá o její vykreslení (namalování). Mezi tím není žádný mezikrok s bitmapami. Čára, kterou nakreslíte pomocí MoveTo a LineTo, nakonec skončí jako operátory cesty PDF v toku obsahu. Díky tomu zůstává skutečným vektorem: ostrá při 50% zvětšení, ostrá při 1600% zvětšení, a se zlomkovou velikostí oproti tomu, co by stála rastrovaná verze. Pro diagramy, oddělovače v tabulkách, osy grafů a dekorace formulářů je to přesně to, co chcete, a API, které se za tím skrývá, je natolik malé, že se ho naučíte za jedno odpoledne
Celá kreslicí plocha žije na THotPDF.CurrentPage. Mezi voláními BeginDoc a EndDoc můžete na tomto objektu stránky nastavit barvu a tloušťku čáry, definovat geometrii a zavolat operátor malování, který to potvrdí. Čtyři primitiva, která budete používat nejčastěji, jsou MoveTo a LineTo pro libovolné cesty, Rectangle pro boxy, Circle pro disky a dva operátory malování: tah (Stroke) a výplň (Fill)
Souřadnicový systém je zleva zdola (bottom-left)
To je věc, která zaskočí snad každého, kdo přichází z prostředí VCL. TCanvas, kterým vykreslujete ovládací prvky, umisťuje počátek do levého horního rohu, přičemž Y roste směrem dolů. PDF dělá přesný opak. HotPDF měří body (1/72 palce) od levého dolního rohu stránky, přičemž Y se zvyšuje směrem nahoru. Bod na souřadnici Y := 720 leží někde poblíž horního okraje stránky formátu US Letter, která je vysoká 792 bodů, a Y := 50 leží blízko spodního okraje. Pokud vám vaše první kresba vyjde vertikálně zrcadlově obrácená, je to z tohoto důvodu: kód přenesený z grafiky na obrazovce předpokládá špatný směr a "vyjede" ze spodního okraje
Téže konvenci podléhá i TextOut, takže text a tvary sdílejí po osvojení společný mentální model. Plánujte rozvržení tak, že se rozhodnete, kde bude sedět spodní část každého prvku, nikoliv jeho vršek, a zbytek už bude následovat
Cesty: MoveTo, LineTo, Stroke
Cesta ohraničená tahem znamená: pero je zvednuto, položeno a taženo. MoveTo zvedne pero a určí počáteční bod, aniž by se něco nakreslilo. Každé volání LineTo pak rozšiřuje aktuální cestu k novému bodu. Na stránce se neobjeví absolutně nic, dokud nezavoláte Stroke. Tím se vykreslí nahromaděná cesta pomocí aktuální barvy tahu a tloušťky čáry. Následně se cesta vyčistí, takže další MoveTo může začít nanovo
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'DrawPaths.pdf';
Pdf.BeginDoc;
// Line width is in points and applies until you change it.
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
// A horizontal rule near the top of the page (Y measured from bottom).
Pdf.CurrentPage.MoveTo(72, 720);
Pdf.CurrentPage.LineTo(523, 720);
Pdf.CurrentPage.Stroke; // commit the path; nothing drew before this
// A thicker connected polyline: three segments in one path.
Pdf.CurrentPage.SetLineWidth(3);
Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
Pdf.CurrentPage.MoveTo(72, 640);
Pdf.CurrentPage.LineTo(172, 690);
Pdf.CurrentPage.LineTo(272, 620);
Pdf.CurrentPage.LineTo(372, 680);
Pdf.CurrentPage.Stroke;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Dva detaily, které vám ušetří spoustu času při ladění. Tloušťka čáry je stavová hodnota, ne argument: volání SetLineWidth ji nastaví pouze jednou a každé další volání Stroke pak použije tuto hodnotu, dokud ji opět nezměníte. Z toho důvodu je spojená čára výše mnohem tlustší než ta předchozí. Navíc, cesta se po každém zavolání Stroke resetuje. Pokud tedy zapomenete zavolat Stroke, pečlivě nakreslená geometrie se na výstupu vůbec neprokreslí. Chybí-li tvar na výstupu, první místo, kam byste se měli podívat, je malovací operátor
Souřadnice se uvádějí v bodech (points), a ty umí být zlomkové. Funkce MoveTo i LineTo proto přijímají hodnoty typu Single, z čehož vyplývá, že vlasová čára u tloušťky 0.5 bodů nebo pozice na 72.25 jsou plně legální a smysluplné, nikoliv zaokrouhlené na nejbližší celou jednotku. Tato přesnost hraje důležitou roli ve dvou protichůdných směrech. Tloušťka čáry menší než zhruba 0.5 bodu se může na obrazovce vykreslit jako nejtenčí možná zařízení-závislá čára, která na obrazovce může zcela zmizet a objevit se až po vytištění. Viditelná linie proto vyžaduje tloušťku nastavenou úmyslně, nikoliv s využitím výchozích hodnot. Na druhou stranu, přichytávání mřížek a linek tabulek k celočíselným bodovým souřadnicím zabrání tomu, aby hustá mřížka vypadala mírně nerovnoměrně kvůli různému zaokrouhlování přilehlých čar. Rozhodněte o rozestupech mřížky v bodech už od začátku a zbytek rozložení pak tuto volbu zdědí
Vyplněné tvary a barva
Uzavřená primitiva mohou být spíše vyplněna než orámována. Funkce Rectangle přebírá polohu a velikost, Circle vezme střed a poloměr a obojí se pak potvrdí voláním Fill, které namaluje vnitřek aktuální barvou výplně, anebo voláním Stroke pro generování pouze obrysu. Barva výplně a barva tahu představují dva oddělené kusy stavu. Nastavují se funkcemi SetRGBFillColor a SetRGBStrokeColor, z nichž každá přebírá jedno TColor. Díky tomu je možné přímo využít barvy z Delphi a také pomocníka RGB
// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;
// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;
// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;
Dávejte pozor na formát parametrů u funkce Rectangle. Očekává se pozice a velikost, X, Y, Width, Height, a ne dva protilehlé rohy. TCanvas.Rectangle, na který jsou vývojáři Delphi zvyklí, pracuje se zápisem (Left, Top, Right, Bottom). Svalová paměť tak často poskytne HotPDF druhý roh v místech, kde je očekávána šířka a výška, a rámeček vzejde ve zcela špatných rozměrech. Pár (X, Y) vždy představuje levý dolní roh, což je plně konzistentní s počátkem stránky. Pro nakreslení kružnice se bodem (X, Y) stává její střed a třetím argumentem je poloměr v bodech
Jedna barevná volba, ve které původní ukázka chybovala
Starší verze tohoto příkladu vybírala barvy u každého tvaru generováním přes Random($FFFFFF). Vypadá to živě a efektně, jenže pro generování dokumentů je to špatný instinkt. PDF soubor, který programově stavíte z kódu, typicky chcete rovněž testovat. Náhodné barvy výplně pak znemožňují srovnání mezi jednotlivými běhy: bajt-po-bajtu provedený diff vůči dříve fungující verzi selže pokaždé, bez jakéhokoli skutečného opodstatnění. Zvolte proto explicitní barvy. Pokud potřebujete více variant ve sledu po sobě, vytvořte fixní pole barevné palety, popřípadě to řiďte pomocí závislosti od skutečných dat. Pak stejný vstup bude produkovat přesně ty samé výstupy. Determinizmus má mnohem větší hodnotu než originalita a u generovaných artefaktů během fáze vydávání to platí dvojnásob
Sestavení primitiv do jednoho celku: Box s popisky (Callout)
Každé ze základních primitiv je samo o sobě zcela prosté. Užitečnost těchto prvků se ale naplno projeví teprve v momentě, kdy celou skupinu prvků použijete společně na to, co skutečný report ve finále reálně potřebuje. Callout, popsaný rámeček fungující coby odkazující anotace ke grafice, využívá hned všeho, čím jsme prošli výše: vyplněný obdélník s ohraničením, taženou linku popisku, tečku k ukotvení a také text vložený přímo dovnitř tohoto pole, operující pod stejným systémem počátku od spodního levého okraje. Operace FillAndStroke si zde zaručeně vyslouží své místo – namalování vnitřku a ohraničení jedné a té samé cesty dokáže udělat v rámci jediného potvrzení (commit), namísto toho, aby se obdélník sestavoval dvakrát
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := 'Callout.pdf';
Pdf.BeginDoc;
// 1. The box: pale fill plus a visible border, one path, one commit.
// Rectangle is lower-left corner plus size, Y measured from the bottom
Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214)); // pale amber panel
Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40)); // darker rim
Pdf.CurrentPage.SetLineWidth(1);
Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
Pdf.CurrentPage.FillAndStroke;
// 2. The pointer: one stroked segment from the box edge down
// toward the thing being annotated
Pdf.CurrentPage.SetLineWidth(1.5);
Pdf.CurrentPage.MoveTo(90, 615); // left edge of the box
Pdf.CurrentPage.LineTo(66, 546);
Pdf.CurrentPage.Stroke;
// 3. A filled dot anchors the pointer at its target
Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
Pdf.CurrentPage.Circle(64, 542, 3);
Pdf.CurrentPage.Fill;
// 4. The label, positioned relative to the box's lower-left corner.
// Text and shapes share one coordinate system, so the offsets
// are plain arithmetic against (90, 600)
Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Všimněte si, jak málo stavu toto složenější řešení potřebuje. Barva výplně, tahu a šířka linky jsou nastaveny těsně před tvarem, který je používá, takže se každý blok kresby dá číst jako samostatná jednotka. Může být přeřazen či přesunut do pomocné metody, aniž by s sebou táhl jakýkoli skrytý stav. Pokud toto zabalíte do procedury přebírající kotvící bod a řetězce, máte rázem připraven opakovatelně použitelný prvek pro anotaci diagramu, to vše za cenu pouhých čtyřiceti řádků kódu
Kdy se vyplácí generovat vektory a kdy nikoliv
Sáhněte po těchto voláních cest a tvarů ve všech případech, kdy je generována geometrie: pomocné mřížky grafů, linky ohraničující řádky u tabulky s fakturou, callout boxy u diagramů nebo logo značky vyjádřené jako hrstka cest (křivek). Vše se navíc dokáže škálovat bez jakéhokoli rozmazání okrajů, a co do velikosti souboru to nezabírá prakticky nic, jelikož obdélník je reprezentován pouze několika málo čísly namísto tisíců pixelů. Opačná stránka je ale také reálná. Máte-li skutečnou fotografii anebo screenshot, umístěte je nakreslením obrázku raději napřímo pomocí AddImage a ShowImage. Obkreslování (trasování) bitmapy s pomocí vektorových křivek vám totiž nepřinese absolutně žádný užitek. Pro převážnou většinu reálné reportovací práce postačují výše uvedené rovné úseky, obdélníky a kruhy. Tři další vylepšení, o která vývojáři často žádají jako první – křivky, přerušované vzory čar a průhlednost – sídlí taktéž na tom stejném objektu stránky
Křivky, přerušované čáry a průhlednost ve zkratce
Tvorba křivek (freeform curves) rozšiřuje tu samou cestu, kterou již používáte. Volání CurveToC(X1, Y1, X2, Y2, X3, Y3) připojí z aktuálního bodu další kubický Bezierův segment k cíli (X3, Y3), ohýbající se směrem k oněm dvěma kontrolním (usměrňujícím) bodům. Rychlé varianty CurveToV a CurveToY pak pokrývají případy, kdy se jeden kontrolní bod překrývá s koncovým bodem úseku. Cesta dokáže mixovat segmenty LineTo a CurveToC zcela volně, než to celé jeden příkaz Stroke nebo Fill potvrdí. A to je mimochodem způsob, jak se tvoří tvary jako jsou zaoblené rohy a hladké linie grafů
Přerušované vykreslování tahů představuje stavový režim, přesně stejně jako tloušťka linky. SetDash([3, 3], 0) přepne jakýkoliv následující tah na vzor se zápisem 3 body pera na plátno, 3 body díra bez zápisu. Pole v argumentu tedy definuje délky cyklů v bodech a druhý argument fázi (posun), ze které samotný cyklus začíná. Parametr NoDash vrátí pero zpět do podoby plné neustále pevné čáry. Nastavte si režim, namalujte linie mřížky a navraťte režim do pevné čáry (reset) předtím, než namalujete další linii – jinak vám přerušovaný vzor potichu infikuje úplně všechno, co následuje potom
Průhlednost, na rozdíl od klasického barevného argumentu, je aplikována přes pojmenovaný grafický stav, protože hodnota alfa filtru v PDF se řídí parametry grafického stavového slovníku (graphics state dictionary). Zaregistrujte ho pro dokument voláním RegisterExtGState společně s parametry propustnosti (fill alfa a stroke alfa) v rozsahu od 0 do 1 a následně pak vraťte tento identifikátor (jméno, které metoda vrácí) objektu stránky přes zavolání příkazu CurrentPage.SetGraphicsState; všechny tahy i výplně prováděné od tohoto bodu se namalují s takto zaregistrovanou průhledností. Ač to znamená trochu složitější uspořádání kódu než jen volání nastavovačů barvy, jednoznačně se to vyplatí při první situaci, kdy vám má sloupec se zvýrazněním označit psaný text, aniž by ho tím zcela zakryl
Posledním zvykem, který stojí za to si udržovat, je verifikace. Z kódu generovaná geometrie se u vás na stroji dokáže spustit bez potíží, ale na zákaznickém zařízení se rozbije, většinou z důvodu selhání substituce fontu textu, se kterým jste ji promíchali, nebo kvůli nerespektování parametrů velikosti stránky, které pak v reálu nedrží tvar. Vykreslený PDF soubor otevřete s využitím vícera rozsahů přiblížení k potvrzení čistoty všech vyobrazených hran a rovněž si ověřte, že každý tvar zapadne přesně dovnitř vaší určené krabice k obalu z dokumentu. Při použití deterministického barevného rozvržení může být navíc celý proces této kontroly automatizován pomocí softwarového srovnání s referenčním PDF (namísto spoléhání se na manuální vizuální zkoumání programátorem)
Příkazy MoveTo, LineTo, Stroke, Fill a volání k nastavování barev zobrazená zde jsou součástí komponenty HotPDF pro Delphi a C++Builder