Technical Article

Vektorová grafika v PDF s Delphi: Cesty a přechody

Většina kódu v Delphi, který pracuje s PDF, přistupuje k tomuto formátu jako ke kontejneru pro dvě věci: bloky textu a několik umístěných bitmap. Tento pohled je sice v základu správný, ale ponechává nejvýkonnější část formátu nevyužitou. Stránka PDF je na rozlišení nezávislé 2D plátno postavené na stejném zobrazovacím modelu jako PostScript. Dokáže kreslit čáry, křivky, vyplněné oblasti, přechody a opakující se vzory, to vše jako vektory, které zůstávají ostré při jakémkoli přiblížení a tisknou se v plném rozlišení zařízení. Pokud kreslíte logo, graf, vodoznak nebo okraj certifikátu, vektorová cesta je téměř vždy tím správným primitivem a je menší a ostřejší než rastrovaný obrázek, po kterém místo toho mnoho programů sahá.

Tento článek provází vektorovým modelem tak, jak jej definuje ISO 32000-1, a ukazuje odpovídající volání PDFlibPas. Cílem je učinit specifikaci konkrétní, protože rozhraní API se na ni úzce mapuje a pochopení jednoho vás naučí to druhé.

Stránka je stroj na cesty

ISO 32000-1 §8.5 popisuje grafiku ve dvou fázích, které se nikdy nepřekrývají. Nejprve sestavíte cestu (path), což je čistá geometrie bez viditelného výsledku. Poté tuto cestu vykreslíte v jediné operaci, která obtáhne její obrys, vyplní její vnitřek, nebo provede obojí. Během konstrukce se na stránce nic neobjeví. Cesta je abstraktní sekvence bodů a segmentů udržovaná v grafickém stavu, dokud ji nespotřebuje vykreslovací operátor; v tom okamžiku se vykreslí a zahodí.

Cesta se skládá z jedné nebo více podcest (subpaths). Podcesta začíná v bodě a roste připojováním segmentů: přímých čar, kubických Bezierových křivek a na některých platformách celých obdélníků přidaných jako jejich vlastní uzavřená podcesta. V PDFlibPas otevřete cestu pomocí StartPath, což nastaví výchozí bod, a poté ji rozšíříte pomocí AddLineToPath a AddCurveToPath. Každé volání posouvá implicitní aktuální bod, takže další segment pokračuje od místa, kde předchozí skončil. ClosePath nakreslí závěrečný přímý segment zpět na začátek podcesty, což je důležité pro obtahování, protože to vytváří skutečné spojení čar v uzavíracím vrcholu namísto dvou volných konců.

// A closed quadrilateral, stroked then filled
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // open the path at the first vertex
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // straight segment back to (150, 100)
PDF.DrawPath(2);                   // 2 = fill and stroke; path is consumed

Křivky používají AddCurveToPath, které přebírá dva Bezierovy řídicí body a koncový bod: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Křivka vede z aktuálního bodu do bodu (EndX, EndY) a je tažena směrem ke dvěma řídicím bodům. Kruhové oblouky jsou k dispozici prostřednictvím AddArcToPath(CenterX, CenterY, TotalAngle), kde se poloměr bere jako vzdálenost mezi aktuálním bodem a středem a engine emituje oblouk jako řetězec Bezierových segmentů. Obdélníky mají zkratku AddBoxToPath(Left, Top, Width, Height), která připojí kompletní uzavřený obdélník jako jeho vlastní podcestu bez předchozího StartPath.

Dvě pravidla výplně a proč se liší

Při vyplňování cesty, která se sama kříží nebo obsahuje vnitřní smyčku, potřebuje vykreslovací modul pravidlo pro určení, které oblasti jsou uvnitř tvaru a které jsou otvory. ISO 32000-1 §8.5.3.3 definuje dvě pravidla, která mohou stejnou geometrii vykreslit odlišně. Pravidlo nenulového oběhu (nonzero winding rule) počítá znaménková křížení paprsku vyslaného z testovacího bodu do nekonečna, přičemž připočítává jedničku za každý segment, který kříží zleva doprava, a odečítá jedničku za každý segment křížící opačně; bod je uvnitř, pokud součet není nula. Pravidlo sudá-lichá (even-odd rule) ignoruje směr a jednoduše počítá křížení, přičemž bod považuje za vnitřní, pokud je počet křížení lichý.

Klasickým případem, kdy se rozcházejí, je tvar s otvorem, například mezikruží. Nakreslete vnější hranici a vnitřní hranici uvnitř ní. Podle pravidla sudá-lichá vnitřní smyčka vždy vyřízne otvor, protože jakýkoli bod mezi oběma hranicemi je překročen jednou a jakýkoli bod uvnitř vnitřní smyčky je překročen dvakrát. Podle pravidla nenulového oběhu se otvor objeví pouze tehdy, pokud se vnitřní smyčka odvíjí v opačném směru než vnější; pokud se odvíjejí stejným směrem, oběhy se posilují, místo aby se vyrušily, a vnitřní oblast se vyplní jako plná. Pěticípá hvězda nakreslená jako jeden sebeprotínající se obrys vykazuje stejné rozdělení: pravidlo sudá-lichá ponechává centrální pětiúhelník prázdný, zatímco pravidlo nenulového oběhu jej vyplní.

PDFlibPas vyhírá pravidlo podle volání, které provedete pro vykreslení, nikoli podle příznaku. DrawPath vyplňuje podle pravidla nenulového oběhu; DrawPathEvenOdd vyplňuje podle pravidla sudá-lichá. Obě metody přebírají stejný celočíselný režim: 0 pouze obtáhne obrys, 1 pouze vyplní a 2 vyplní i obtáhne. Pravidlo sudá-lichá je snazším nástrojem pro vyřezávání otvorů právě proto, že po vás nevyžaduje správu směru podcest.

// Same two boxes, two fill rules, two different results.
// Nonzero winding: both boxes wind the same way, so the inner one
// does NOT cut a hole and the whole outer box fills solid.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 100, 200, 120);   // outer
PDF.AddBoxToPath(140, 130, 120,  60);   // inner
PDF.DrawPath(1);                         // 1 = fill, nonzero winding

// Even-odd: the inner box is crossed an even number of times,
// so it punches a clean rectangular hole through the outer box.
PDF.SetFillColor(0.2, 0.4, 0.8);
PDF.AddBoxToPath(100, 300, 200, 120);   // outer
PDF.AddBoxToPath(140, 330, 120,  60);   // inner cut-out
PDF.DrawPathEvenOdd(1);                  // 1 = fill, even-odd

Axiální přechody mění barvu podél přímky

Jednolitá barva výplně (flat fill color) má jednu hodnotu v celé oblasti. Přechod (gradient) mění barvu plynule a nejjednodušším typem je axiální neboli lineární přechod. ISO 32000-1 §8.7.4.5 jej specifikuje jako axiální stínování typu 2 (Type 2 axial shading): zadáte dva body, které definují osu, počáteční barvu v prvním bodě a koncovou barvu ve druhém bodě, a vykreslovací modul interpoluje barvu podél této osy. Caždý bod ve vyplněné oblasti přebírá barvu své kolmé projekce na osu, takže přechod probíhá v pásech kolmých k přímce mezi oběma body.

V PDFlibPas je přechod pojmenovaným prostředkem dokumentu, který vytvoříte jednou a poté jej vyberete jako aktivní barvu. Zaregistruje jej NewRGBAxialShader. Signatura je NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dva koncové body osy, RGB trojice na každém konci jako hodnoty v rozsahu 0 až 1 a příznak Extend. S hodnotou Extend nastavenou na 1 pokračují koncové barvy jako plná výplň i za koncové body osy, což je obvykle žádoucí, aby rohy oblasti mimo osu nezůstaly nepomalované; hodnota 0 je ponechá beze změny. Jakmile shader existuje, svážete jej pomocí SetFillShader pro vyplněné oblasti, SetLineShader pro obtahované obrysy nebo SetTextShader pro text. Toto svázání zůstává aktivní pro následná volání kreslení, takže cesta, kterou vykreslíte příště, převezme přechod namísto jednolité barvy.

// Define a vertical gradient once: blue at the bottom to white at the top.
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // start point and start RGB
  0, 260,   1.00, 1.00, 1.00,    // end point and end RGB
  1);                            // 1 = extend ends as solid color

// Select the gradient as the fill, then paint a rectangle with it.
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = fill, now filled by the shader

Osa je zde svislá, od y=100 do y=260 při pevném x, takže barevné pásy probíhají vodorovně a obdélník přechází z modré u své základny do bílé nahoře. Vzhledem k tomu, že shader je klíčován názvem, může jedna definice vyplnit libovolný počet tvarů na stránce a přepnutí zpět na jednolitou barvu je jen dalším voláním SetFillColor před další cestou.

Opakující se vzory opakují buňku

Zatímco přechod plynule mění jednu barvu, opakující se vzor (tiling pattern) opakuje malou část grafiky v celé oblasti. ISO 32000-1 §8.7.3.1 definuje opakující se vzor jako buňku vzoru (pattern cell), nezávislý prvek obsahu, který vykreslovací modul replikuje na pevné mřížce, aby vydláždil kreslenou oblast. Tímto způsobem vytváříte šrafování pro technické výplně, opakující se motiv značky za hlavičkou nebo texturované pozadí, které zůstává vektorově ostré a nezatěžuje soubor bez ohledu na velikost oblasti, protože buňka je uložena pouze jednou a odkazuje se na ni všude.

PDFlibPas sestavuje buňku vzoru ze zachyceného obsahu stránky. Stránku nebo oblast zachytíte pomocí CapturePage, přeměníte zachycený obsah na pojmenovaný vzor pomocí NewTilingPatternFromCapturedPage(PatternName, CaptureID) a poté tento vzor vyberete jako aktuální výplň pomocí SetFillTilingPattern(PatternName). Od tohoto okamžiku je jakákoli cesta, kterou vyplníte, vykreslena s opakující se buňkou namísto jednolité barvy, což funguje přesně jako výplň shaderem, ale jako zdroj barvy slouží dlaždicová buňka. Tato sekvence je složitější než jediné volání, takže pokud je pro vás krok zachycení nový, přistupujte ke vzoru jako ke dvoufázové operaci: nejprve vytvořte zachycenou buňku a před kreslením oblasti, kterou chcete vydláždit, ji navažte jako výplň podle názvu.

Skládání primitiv dohromady

Jednotlivé části se skládají přímo. Vyplněný Bezierův objekt je cesta křivek vykreslená pomocí DrawPath. Stejný obrys vykreslený pomocí DrawPathEvenOdd po přidání vnitřní smyčky vykazuje otvor, který by výplň podle oběhu uzavřela. Obdélník vyplněný přechodem je box navázaný na shader. Níže uvedený příklad vykresluje všechny tři v pořadí, takže rozdíl mezi oběma pravidly výplně je viditelný na jedné stránce, a poté pod ně pokládá panel s přechodem.

// 1. A filled Bezier shape (nonzero winding).
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 480);
PDF.AddCurveToPath(160, 560, 240, 560, 280, 480);   // top lobe
PDF.AddCurveToPath(240, 420, 160, 420, 120, 480);   // bottom lobe
PDF.ClosePath;
PDF.DrawPath(1);                                     // 1 = fill

// 2. The same outline, plus an inner loop, filled even-odd to show a hole.
PDF.SetFillColor(0.85, 0.30, 0.25);
PDF.StartPath(120, 300);
PDF.AddCurveToPath(160, 380, 240, 380, 280, 300);
PDF.AddCurveToPath(240, 240, 160, 240, 120, 300);
PDF.ClosePath;
PDF.MovePath(180, 300);                              // new subpath: the hole
PDF.AddArcToPath(200, 300, 360);                     // a full circle
PDF.ClosePath;
PDF.DrawPathEvenOdd(1);                              // hole is punched out

// 3. A rectangle filled with an axial gradient.
PDF.NewRGBAxialShader('footerGrad',
  60, 100,  0.95, 0.55, 0.10,
  60, 200,  0.20, 0.10, 0.40,
  1);
PDF.SetFillShader('footerGrad');
PDF.AddBoxToPath(60, 100, 340, 100);
PDF.DrawPath(1);

Dva detaily stojí za zapamatování. Vykreslovací volání rozhoduje o pravidle výplně, takže volba mezi DrawPath a DrawPathEvenOdd je volbou mezi nenulovým oběhem a sudá-lichá, přičemž u tvarů s otvory vás pravidlo sudá-lichá ušetří přemýšlení o směru podcest. A stav grafiky je načten v okamžiku vykreslení: nastavte barvy, šířku čáry a vazbu shaderu před voláním kreslení, protože to je stav, který engine čte. Nejprve sestavit, poté nakonfigurovat stav a nakonec vykreslit; tak se vektorový model chová vždy předvídatelně.

Odtud jsou přirozenými dalšími kroky čtení vektorů a textu zpět z existujícího dokumentu, což je popsáno v našem článku o extrakci textu, obrázků a písem, a vykreslování stejného kreslicího modelu do kontextu zařízení Windows pro náhled na obrazovce a tisk, což rozebírá průvodce tiskem a náhledem. Volání cest, shaderů a vzorů popsaná zde jsou dodávána jako součást Delphi PDF Library spolu s rozhraními API pro text, obrázky, formuláře a podpisy, o nichž pojednáváme na jiných místech tohoto blogu.