Technical Article

Vektorová grafika v PDF s Delphi: Cesty a prechody

Väčšina kódu v Delphi, ktorý pracuje s PDF, zaobchádza s týmto formátom ako s kontajnerom pre dve veci: bloky textu a niekoľko umiestnených bitmáp. Tento pohľad je síce správny, ale najschopnejšiu časť formátu ponecháva nevyužitú. Stránka PDF je 2D plátno nezávislé od rozlíšenia, postavené na rovnakom zobrazovacom modeli ako PostScript. Dokáže kresliť čiary, krivky, vyplnené oblasti, prechody a opakujúce sa vzory, všetko ako vektory, ktoré zostávajú ostré pri akomkoľvek priblížení a tlačia sa v plnom rozlíšení zariadenia. Ak kreslíte logo, graf, vodotlač alebo okraj certifikátu, vektorová cesta je takmer vždy tým správnym primitívom a je menšia a ostrejšia ako rasterizovaný obrázok, po ktorom namiesto toho siaha mnoho programov.

Tento článok prechádza vektorový model tak, ako ho definuje norma ISO 32000-1, a ukazuje zodpovedajúce volania PDFlibPas. Cieľom je konkretizovať špecifikáciu, pretože API sa na ňu úzko mapuje a pochopenie jedného vás naučí to druhé.

Stránka je stroj na cesty

ISO 32000-1 §8.5 popisuje grafiku v dvoch fázach, ktoré sa nikdy neprekrývajú. Najprv zostavíte cestu, čo je čistá geometria bez viditeľného výsledku. Potom túto cestu vykreslíte v jedinej operácii, ktorá vykreslí jej obrys, vyplní jej vnútro alebo urobí oboje. Počas konštrukcie sa na stránke nič neobjaví. Cesta je abstraktná sekvencia bodov a segmentov uchovávaná v grafickom stave, kým ju nespotrebuje vykresľovací operátor, kedy sa vykreslí a zahodí.

Cesta sa skladá z jednej alebo viacerých podciest. Podcesta začína v bode a rastie pripájaním segmentov: priamych čiar, kubických Bézierových kriviek a na niektorých platformách celých obdĺžnikov pridaných ako vlastná uzavretá podcesta. V PDFlibPas otvárate cestu pomocou StartPath, čo nastaví počiatočný bod, a potom ju rozširujete pomocou AddLineToPath a AddCurveToPath. Každé volanie posúva implicitný aktuálny bod, takže ďalší segment pokračuje tam, kde predchádzajúci skončil. ClosePath nakreslí záverečný priamy segment späť na začiatok podcesty, na čom záleží pri vykresľovaní obrysu, pretože to vytvára skutočné spojenie čiar na uzatváracom vrchole namiesto dvoch voľných koncoviek.

// 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

Krivky používajú AddCurveToPath, ktoré berie dva riadiace body Bézierovej krivky a koncový bod: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Krivka prebieha od aktuálneho bodu po (EndX, EndY), pričom je počas cesty ťahaná k dvom riadiacim bodom. Kruhové oblúky sú dostupné prostredníctvom AddArcToPath(CenterX, CenterY, TotalAngle), kde sa polomer berie zo vzdialenosti medzi aktuálnym bodom a stredom, a engine vygeneruje oblúk ako reťazec Bézierových segmentov. Obdĺžniky majú skratku AddBoxToPath(Left, Top, Width, Height), ktorá pripojí kompletný uzavretý obdĺžnik ako vlastnú podcestu bez predchádzajúceho StartPath.

Dve pravidlá výplne a prečo sa nezhodujú

Keď vypĺňate cestu, ktorá sa kríži alebo obsahuje vnútornú slučku, vykresľovač potrebuje pravidlo na rozhodnutie, ktoré oblasti sú vo vnútri tvaru a ktoré sú otvormi. Norma ISO 32000-1 §8.5.3.3 definuje dve a tie môžu rovnakú geometriu vykresliť odlišne. Pravidlo nenulového vinutia (nonzero winding rule) počíta znamienkové kríženia lúča vrhnutého z testovacieho bodu do nekonečna, pričom pripočíta jednotku za každý segment, ktorý lúč pretína zľava doprava, a odpočíta jednotku za každý, ktorý ho pretína opačne; bod je vo vnútri, keď celkový súčet nie je nula. Pravidlo párny-nepárny (even-odd rule) ignoruje smer a jednoducho počíta kríženia, pričom označuje bod za vnútorný, keď je počet krížení nepárny.

Klasickým prípadom, kedy sa rozchádzajú, je tvar s otvorom, ako šiška (donut) alebo podložka. Nakreslite vonkajšiu hranicu a vnútornú hranicu vo vnútri. Podľa pravidla párny-nepárny vnútorná slučka vždy vytvorí otvor, pretože akýkoľvek bod medzi dvoma hranicami je preťatý raz a akýkoľvek bod vo vnútri vnútornej slučky je preťatý dvakrát. Podľa pravidla nenulového vinutia sa otvor objaví len vtedy, ak sa vnútorná slučka vinie opačným smerom ako vonkajšia; ak ich naviniete rovnakým smerom, vinutia sa posilnia namiesto toho, aby sa vyrušili, a vnútorná oblasť sa vyplní celá. Päťramenná hviezda nakreslená ako jeden samopretínajúci sa obrys ukazuje rovnaké rozdelenie: párny-nepárny ponecháva stredový päťuholník prázdny, zatiaľ čo nenulové vinutie ho vyplní.

PDFlibPas vyberá pravidlo podľa volania, ktoré urobíte na vykreslenie, nie podľa príznaku. DrawPath vypĺňa podľa pravidla nenulového vinutia; DrawPathEvenOdd vypĺňa podľa pravidla párny-nepárny. Obe berú rovnaký celočíselný režim: 0 len obtiahne obrys, 1 len vyplní a 2 vyplní aj obtiahne. Pravidlo párny-nepárny je jednoduchším nástrojom pre vyrezávanie otvorov práve preto, že od vás nevyžaduje riadenie smeru podcesty.

// 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álne prechody menia farbu pozdĺž čiary

Plochá farba výplne má jednu hodnotu v celej oblasti. Prechod mení farbu plynule a najjednoduchším druhom je axiálny alebo lineárny prechod. ISO 32000-1 §8.7.4.5 ho špecifikuje ako axiálne tieňovanie typu 2 (Type 2 axial shading): zadáte dva body, ktoré definujú os, počiatočnú farbu v prvom bode a koncovú farbu v druhom bode, a vykresľovač interpoluje farbu pozdĺž tejto osi. Každý bod vo vyplnenej oblasti preberá farbu svojej kolmej projekcie na os, takže prechod prebieha v pásoch kolmých na spojnicu týchto dvoch bodov.

V PDFlibPas je prechod pomenovaným zdrojom dokumentu, ktorý vytvoríte raz a potom ho vyberiete ako aktívnu farbu. Zaregistruje ho NewRGBAxialShader. Signatúra je NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dva koncové body osi, trojice RGB na každom konci ako hodnoty v rozsahu 0 až 1 a príznak Extend. S nastaveným Extend na 1 koncové farby pokračujú ako plná výplň za koncovými bodmi osi, čo je zvyčajne to, čo chcete, aby rohy oblasti mimo osi nezostali nenatrete; 0 ich ponechá nedotknuté. Akonáhle shader existuje, prepojíte ho pomocou SetFillShader pre vyplnené oblasti, SetLineShader pre obrysy alebo SetTextShader pre text. Prepojenie zostáva aktívne pre nasledujúce volania kreslenia, takže cesta, ktorú nakreslíte ako ďalšiu, prevezme prechod namiesto plochej farby.

// 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

Os je tu vertikálna, od y=100 po y=260 pri fixnom x, takže farebné pásy prebiehajú horizontálne a obdĺžnik prechádza z modrej farby na svojej základni do bielej na vrchu. Keďže shader je kľúčovaný podľa názvu, jedna definícia môže vyplniť ľubovoľný počet tvarov na stránke a prepnutie späť na plochú farbu je len ďalším volaním SetFillColor pred nasledujúcou cestou.

Opakujúce sa vzory (tiling patterns) opakujú bunku

Zatiaľ čo prechod mení jednu farbu hladko, opakujúci sa vzor (tiling pattern) opakuje malý kúsok grafiky naprieč oblasťou. ISO 32000-1 §8.7.3.1 definuje vzor ako bunku vzoru (pattern cell), nezávislý kúsok obsahu, ktorý vykresľovač replikuje na pevnej mriežke na pokrytie maľovanej oblasti. Takto vytvárate šrafovanie pre technické výplne, opakujúci sa motív značky za hlavičkou alebo textúrované pozadie, ktoré zostáva vektorovo ostré a neváži takmer nič bez ohľadu na to, aká veľká je oblasť, pretože bunka sa uloží raz a odkazuje sa na ňu všade.

PDFlibPas vytvára bunku vzoru zo zachyteného obsahu stránky. Zachytíte stránku alebo oblasť pomocou CapturePage, zmeníte zachytený obsah na pomenovaný vzor pomocou NewTilingPatternFromCapturedPage(PatternName, CaptureID) a potom vyberiete tento vzor ako aktuálnu výplň pomocou SetFillTilingPattern(PatternName). Od tohto momentu sa akákoľvek cesta, ktorú vyplníte, vykreslí s opakujúcou sa bunkou namiesto plochej farby, presne tak, ako funguje výplň shaderom, ale so vzorovou bunkou ako zdrojom farby. Postupnosť je zložitejšia ako jedno volanie, takže ak vám krok zachytenia nie je známy, považujte vzor za dvojfázovú operáciu: najprv vygenerujte zachytenú bunku a potom ju prepojte ako výplň podľa názvu pred vykreslením oblasti, ktorú chcete pokrytú vzorom.

Spájanie primitívov dohromady

Kusy sa skladajú priamo. Vyplnený Bézierov objekt je cesta kriviek nakreslená pomocou DrawPath. Rovnaký obrys nakreslený pomocou DrawPathEvenOdd po pridaní vnútornej slučky ukazuje otvor, ktorý by výplň podľa pravidla vinutia uzavrela. Obdĺžnik vyplnený prechodom je box prepojený so shaderom. Nasledujúci príklad vykresľuje všetky tri v poradí, aby bol rozdiel medzi dvoma pravidlami výplne viditeľný na jednej stránke, a potom pod ne položí panel s prechodom.

// 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);

Stojí za to si zapamätať dva detaily. Volanie vykreslenia rozhoduje o pravidle výplne, takže voľba medzi DrawPath a DrawPathEvenOdd je voľbou medzi nenulovým vinutím a párnym-nepárnym, pričom pri tvaroch s otvormi vás pravidlo párny-nepárny ušetrí od uvažovania o smere podcesty. A grafický stav sa sníma v momente, keď vykresľujete: nastavte svoje farby, šírku čiary a väzbu shaderu pred volaním kreslenia, pretože to je stav, ktorý engine číta. Najprv konštruujte, nakonfigurujte stav, vykreslite ako posledné a vektorový model sa zakaždým správa predvídateľne.

Odtiaľto sú prirodzenými ďalšími krokmi čítanie vektorov a textu späť z existujúceho dokumentu, ktorému sa venuje náš článok o extrakcii textu, obrázkov a písiem, a vykreslenie rovnakého kresliaceho modelu do kontextu zariadenia Windows pre náhľad na obrazovke a tlač, čomu sa venuje sprievodca tlačou a náhľadom. Volania pre cesty, shadery a vzory opísané tu sa dodávajú ako súčasť Delphi PDF Library spolu s API pre text, obrázky, formuláre a podpisy popísanými inde na tomto blogu.