Tekninen artikkeli

Vektorigrafiikka PDF:ssä Delphin avulla: Polut (Paths) ja liukuvärit (Gradients)

Useimmat Delphi-koodit, jotka koskevat PDF-tiedostoja, käsittelevät formaattia säilönä (container) kahdelle asialle: tekstinpätkille ja muutamille sijoitetuille bittikartoille. Tämä näkemys on oikea niin pitkälle kuin se yltää, ja se jättää formaatin kyvykkäimmän osan käyttämättä. PDF-sivu on resoluutiosta riippumaton 2D-kangas (canvas), joka on rakennettu samalle kuvantamismallille (imaging model) kuin PostScript. Se voi piirtää viivoja, käyriä, täytettyjä alueita, liukuvärejä (gradients) ja toistuvia kuvioita (patterns), jotka kaikki ovat vektoreita, jotka pysyvät terävinä millä tahansa zoomauksella ja tulostuvat laitteen täydellä resoluutiolla. Jos piirrät logoa, kaaviota, vesileimaa tai todistuksen reunaa, vektoripolku on lähes aina oikea perusalkio (primitive), ja se on pienempi ja terävämpi kuin rasteroitu kuva, johon monet ohjelmat sen sijaan tarttuvat

Tämä artikkeli käy läpi vektorimallin sellaisena kuin ISO 32000-1 sen määrittelee ja näyttää vastaavat PDFlibPas-kutsut. Tavoitteena on tehdä spesifikaatiosta konkreettinen, koska API (ohjelmointirajapinta) kartoittuu siihen läheisesti, ja toisen ymmärtäminen opettaa sinulle toisen

Sivu on polkukone (path machine)

ISO 32000-1 §8.5 kuvaa grafiikkaa kahdessa vaiheessa, jotka eivät koskaan mene päällekkäin. Ensin rakennat polun, joka on puhdasta geometriaa ilman näkyvää tulosta. Sitten maalaat tuon polun yhdessä operaatiossa, joka piirtää sen ääriviivan (strokes its outline), täyttää sen sisäosan tai tekee molemmat. Mitään ei näy sivulla rakentamisen aikana. Polku on abstrakti sarja pisteitä ja segmenttejä, jota pidetään grafiikkatilassa (graphics state), kunnes maalausoperaattori kuluttaa sen, jolloin se renderöidään ja hylätään

Polku koostuu yhdestä tai useammasta alipolusta (subpath). Alipolku alkaa pisteestä ja kasvaa lisäämällä segmenttejä: suoria viivoja, kuutiollisia Bezier-käyriä ja joillakin alustoilla kokonaisia suorakulmioita lisättynä omana suljettuna alipolkunaan. PDFlibPas-kirjastossa avaat polun komennolla StartPath, joka asettaa aloituspisteen, ja pidennät sitä sitten komennoilla AddLineToPath ja AddCurveToPath. Jokainen kutsu siirtää implisiittistä nykyistä pistettä, joten seuraava segmentti jatkuu siitä, mihin edellinen päättyi. ClosePath piirtää viimeisen suoran segmentin takaisin alipolun alkuun, mikä on tärkeää viivan piirtämisessä (stroking), koska se tuottaa todellisen viivaliitoksen (line join) sulkevaan kärkeen (vertex) kahden irrallisen päätytulpan (end cap) sijaan

// 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äyrät käyttävät komentoa AddCurveToPath, joka ottaa kaksi Bezier-ohjauspistettä (control points) ja päätepisteen (end point): AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Käyrä kulkee nykyisestä pisteestä kohteeseen (EndX, EndY), vetäytyen kohti kahta ohjauspistettä matkan varrella. Ympyränkaaret ovat saatavilla komennolla AddArcToPath(CenterX, CenterY, TotalAngle), jossa säde (radius) otetaan nykyisen pisteen ja keskipisteen välisestä etäisyydestä, ja moottori lähettää kaaren ketjuna Bezier-segmenttejä. Suorakulmioilla on oikotie, AddBoxToPath(Left, Top, Width, Height), joka lisää täydellisen suljetun suorakulmion omana alipolkunaan ilman edeltävää StartPath-komentoa

Kaksi täyttösääntöä (fill rules), ja miksi ne ovat eri mieltä

Kun täytät polun, joka risteää itsensä kanssa tai sisältää sisäisen silmukan, renderöijä tarvitsee säännön, jolla se päättää, mitkä alueet ovat muodon sisällä ja mitkä ovat reikiä. ISO 32000-1 §8.5.3.3 määrittelee kaksi, ja ne voivat maalata saman geometrian eri tavalla. Nollasta poikkeava kiertosääntö (nonzero winding rule) laskee testipisteestä äärettömyyteen heitetyn säteen allekirjoitetut (signed) ylitykset, lisäten yhden jokaiselle segmentille, joka ylittää vasemmalta oikealle, ja vähentäen yhden jokaiselle, joka ylittää toiseen suuntaan; piste on sisäpuolella, kun summa ei ole nolla. Parillinen-pariton -sääntö (even-odd rule) jättää suunnan huomiotta ja yksinkertaisesti laskee ylitykset kutsuen pistettä sisäpuolella olevaksi, kun laskutoimituksen tulos on pariton (odd)

Klassinen tapaus, jossa ne eroavat toisistaan, on muoto, jossa on reikä, donitsi tai aluslevy (washer). Piirrä ulompi raja ja sisempi raja sen sisälle. Parillinen-pariton -säännön (even-odd rule) alaisena sisäinen silmukka kovertaa aina reiän, koska mikä tahansa piste näiden kahden rajan välillä ylitetään kerran, ja mikä tahansa piste sisäisen silmukan sisällä ylitetään kahdesti. Nollasta poikkeavan kiertosäännön (nonzero winding rule) alaisena reikä näkyy vain, jos sisäinen silmukka kiertyy vastakkaiseen suuntaan kuin ulompi; kierrä ne samaan suuntaan, niin kierrot (windings) vahvistavat toisiaan kumoamisen sijaan, ja sisäalue täyttyy kokonaan. Viisisakarainen tähti, joka on piirretty yhtenä itseään leikkaavana (self-intersecting) ääriviivana, näyttää saman jaon: parillinen-pariton jättää keskellä olevan viisikulmion tyhjäksi, kun taas nollasta poikkeava kierto täyttää sen

PDFlibPas valitsee säännön maalaukseen tekemäsi kutsun perusteella, ei lipun (flag) avulla. DrawPath täyttää nollasta poikkeavalla kiertosäännöllä (nonzero winding rule); DrawPathEvenOdd täyttää parillinen-pariton -säännöllä (even-odd rule). Molemmat ottavat saman kokonaislukutilan (integer mode): 0 piirtää vain ääriviivan (strokes), 1 vain täyttää, ja 2 täyttää ja piirtää ääriviivan (fills and strokes). Parillinen-pariton on helpompi työkalu leikattujen reikien (punch-out holes) tekemiseen juuri siksi, että se ei vaadi sinua hallitsemaan alipolun suuntaa (subpath direction)

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

Aksiaaliset liukuvärit (axial gradients) vaihtelevat väriä viivaa pitkin

Tasainen täyttöväri (flat fill color) on yksi arvo koko alueella. Liukuväri vaihtelee väriä jatkuvasti, ja yksinkertaisin tyyppi on aksiaalinen eli lineaarinen liukuväri. ISO 32000-1 §8.7.4.5 määrittelee sen tyypin 2 (Type 2) aksiaaliseksi varjostukseksi (axial shading): annat kaksi pistettä, jotka määrittelevät akselin, aloitusvärin ensimmäisessä pisteessä ja lopetusvärin toisessa, ja renderöijä interpoloi värin kyseistä akselia pitkin. Jokainen täytetyn alueen piste ottaa värin sen kohtisuorasta (perpendicular) projektiosta akselille, joten liukuväri kulkee kaistoina, jotka ovat suorassa kulmassa kahden pisteen väliseen viivaan nähden

PDFlibPas-kirjastossa liukuväri on nimetty asiakirjaresurssi, jonka luot kerran ja valitset sitten aktiiviseksi maaliksi (active paint). NewRGBAxialShader rekisteröi sen. Allekirjoitus (signature) on NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): kaksi akselin päätepistettä, RGB-kolmikot molemmissa päissä arvoina väliltä 0 - 1, ja Extend-lippu. Kun Extend on asetettu arvoon 1, päätyvärit jatkuvat tasaisena täyttönä akselin päätepisteiden yli, mitä yleensä haluat, jotta akselin ulkopuolella olevan alueen kulmia ei jätetä maalaamatta; 0 jättää ne koskemattomiksi. Kun varjostin (shader) on olemassa, sidot sen komennolla SetFillShader täytetyille alueille, SetLineShader piirretyille ääriviivoille (stroked outlines) tai SetTextShader tekstille. Sidonta (binding) pysyy aktiivisena seuraavissa piirtokutsuissa, joten seuraavaksi maalaamasi polku saa liukuvärin tasaisen värin sijaan

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

Tässä akseli on pystysuora, y=100:sta y=260:een kiinteällä x:llä, joten värikaistat (color bands) kulkevat vaakasuunnassa ja suorakulmio haalistuu sinisestä pohjastaan valkoiseksi yläosassaan. Koska varjostimeen (shader) on avattu nimetty yhteys, yksi määritelmä voi täyttää minkä tahansa määrän muotoja sivulla, ja takaisin tasaiseen väriin vaihtaminen on vain uusi SetFillColor-kutsu ennen seuraavaa polkua

Laatoituskuviot (tiling patterns) toistavat solua (cell)

Siinä missä liukuväri vaihtelee yhtä väriä tasaisesti, laatoituskuvio toistaa pienen taideteoksen (artwork) alueen yli. ISO 32000-1 §8.7.3.1 määrittelee laatoituskuvion kuviosoluna (pattern cell), itsenäisenä sisältönä, jota renderöijä toistaa (replicates) kiinteässä ruudukossa laatoittaakseen maalattavan alueen. Näin rakennat viivoituksen (hatching) suunnittelutäyttöä varten, toistuvan brändimotiivin otsikon (header) taakse tai teksturoidun taustan, joka pysyy vektoriterävänä eikä paina juuri mitään riippumatta alueen koosta, koska solu tallennetaan kerran ja siihen viitataan kaikkialla

PDFlibPas rakentaa kuviosolun (pattern cell) siepatusta (captured) sivun sisällöstä. Sieppaat sivun tai alueen komennolla CapturePage, muutat sieppauksen nimetyksi kuvioksi komennolla NewTilingPatternFromCapturedPage(PatternName, CaptureID), ja valitset sitten kyseisen kuvion nykyiseksi täytöksi komennolla SetFillTilingPattern(PatternName). Siitä lähtien mikä tahansa täyttämäsi polku maalataan toistuvalla solulla tasaisen värin sijaan, täsmälleen kuten varjostimen täyttö (shader fill) toimii, mutta laatoitettu solu (tiled cell) on maalin lähde (paint source). Jakso on monimutkaisempi kuin yksittäinen kutsu, joten jos sieppausvaihe (capture step) on tuntematon, käsittele kuviota kaksivaiheisena toimintona: tuota ensin siepattu solu ja sido se sitten täyttönä nimen perusteella ennen kuin piirrät alueen, jonka haluat laatoitettavan

Perusalkioiden (primitives) yhdistäminen

Kappaleet (pieces) muodostavat kokonaisuuden suoraan. Täytetty Bezier-möykky (blob) on käyrien polku maalattuna komennolla DrawPath. Sama ääriviiva maalattuna komennolla DrawPathEvenOdd sisemmän silmukan (inner loop) lisäämisen jälkeen näyttää reiän, jonka kiertotäyttö (winding fill) olisi sulkenut. Liukuvärillä täytetty suorakulmio on laatikko, joka on sidottu varjostimeen. Alla oleva esimerkki piirtää kaikki kolme peräkkäin, jotta ero kahden täyttösäännön välillä näkyy yhdellä sivulla, ja asettaa sitten liukuväripaneelin niiden alle

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

Kaksi yksityiskohtaa on syytä pitää mielessä. Maalauskutsu (painting call) päättää täyttösäännön (fill rule), joten valinta DrawPath:in ja DrawPathEvenOdd:in välillä on valinta nollasta poikkeavan kiertosäännön (nonzero winding) ja parillinen-pariton -säännön (even-odd) välillä, ja reiällisille muodoille parillinen-pariton -sääntö säästää sinut järkeilemästä (reasoning) alipolun suuntaa. Ja grafiikkatila (graphics state) otetaan (sampled) maalaushetkellä: aseta värit, viivan leveys ja varjostinsidos (shader binding) ennen maalauskutsua, koska se on tila (state), jonka moottori lukee. Rakenna ensin, määritä tila, maalaa viimeiseksi, niin vektorimalli käyttäytyy ennustettavasti joka kerta

Tästä luonnolliset seuraavat vaiheet ovat vektorien ja tekstin lukeminen takaisin ulos olemassa olevasta asiakirjasta, mikä käsitellään artikkelissamme pdflibpas-text-image-font-extraction.html, teksti-, kuva- ja fonttien purku (text, image, and font extraction), ja saman piirtomallin (drawing model) renderöinti Windowsin laitekontekstiin (device context) näytöllä esikatselua (on-screen preview) ja tulostusta varten, mikä käsitellään läpikäynnissä pdflibpas-print-preview-device-context.html, tulostus ja esikatselu (print and preview walkthrough). Tässä kuvatut polku-, varjostin- ja kuviokutsut toimitetaan osana Delphi PDF Library -kirjastoa muiden tässä blogissa käsiteltyjen teksti-, kuva-, lomake- ja allekirjoitus-API:den ohella