Tehnični članak

Vektorska grafika v PDF z Delphi: Poti in prelivi

Večina kode v Delphi, ki se dotika PDF, obravnava ta format kot vsebnik za dve stvari: nize besedila in nekaj postavljenih bitnih slik. Ta pogled je sicer pravilen, vendar pušča neuporabljen najbolj zmogljiv del formata. Stran PDF je od ločljivosti neodvisno 2D platno, zgrajeno na enakem slikovnem modelu kot PostScript. Lahko riše črte, krivulje, zapolnjena območja, prelive in ponavljajoče se vzorce, vse kot vektorje, ki ostanejo ostri pri poljubni povečavi in se natisnejo v polni ločljivosti naprave. Če rišete logotip, grafikon, vodni znak ali obrobo certifikata, je vektorska pot skoraj vedno pravi primitiv, poleg tega pa je manjša in ostrejša od rastrske slike, po kateri namesto tega posežejo številni programi

Ta članek obravnava vektorski model, kot ga definira standard ISO 32000-1, in prikazuje ustrezne klice PDF Library for Delphi. Cilj je narediti specifikacijo konkretno, saj se ji API tesno prilagaja, razumevanje enega pa vas nauči drugega

Stran je stroj za poti

ISO 32000-1 §8.5 opisuje grafiko v dveh fazah, ki se nikoli ne prekrivata. Najprej zgradite pot, kar je čista geometrija brez vidnega rezultata. Nato to pot poslikate v eni sami operaciji, ki izriše njeno obrobo, zapolni njeno notranjost ali stori oboje. Med gradnjo se na strani ne prikaže ničesar. Pot je abstraktno zaporedje točk in odsekov, shranjenih v grafičnem stanju, dokler jih slikarski operator ne porabi, takrat pa se izrišejo in zavržejo

PDF Library for Delphi: diagram življenjskega cikla poti PDF, ki pokaže izgradnjo kot nevidno čisto geometrijo, sledi pa ji risalni operator, ki obriše, napolni ali oboje, nato pa pot porabi in zavrže
Gradnja štirikotnika ne naslika nič — klic DrawPath prebere grafično stanje enkrat, izriše obrobo, polnilo ali oboje, in vrže pot stran

Pot je sestavljena iz ene ali več podpoti. Podpot se začne v točki in raste z dodajanjem odsekov: ravnih črt, kubičnih Bezierjevih krivulj in na nekaterih platformah celih pravokotnikov, dodanih kot lastne zaprte podpoti. V PDF Library for Delphi odprete pot s StartPath, ki določi začetno točko, nato pa jo razširite z AddLineToPath in AddCurveToPath. Vsak klic premakne implicitno trenutno točko naprej, tako da se naslednji odsek nadaljuje tam, kjer se je prejšnji končal. ClosePath nariše končni ravni odsek nazaj na začetek podpoti, kar je pomembno pri izrisovanju obrob, saj ustvari pravi stik črt na zapiralnem oglišču namesto dveh ločenih končnih pokrovov

// Zaprt štirikotnik, najprej obrobljen in nato zapolnjen
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // odpri pot pri prvem oglišču
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // raven odsek nazaj do (150, 100)
PDF.DrawPath(2);                   // 2 = zapolni in obrobi; pot je porabljena

Krivulje uporabljajo AddCurveToPath, ki sprejme dve kontrolni točki Bezierja in končno točko: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Krivulja poteka od trenutne točke do (EndX, EndY), na poti pa jo vleče proti obema kontrolnima točkama. Krožni loki so na voljo prek AddArcToPath(CenterX, CenterY, TotalAngle), kjer se polmer vzame iz razdalje med trenutno točko in središčem, pogon pa lok odda kot verigo Bezierjevih odsekov. Pravokotniki imajo bližnjico, AddBoxToPath(Left, Top, Width, Height), ki doda celoten zaprt pravokotnik kot lastno podpot brez predhodnega klica StartPath

Dve pravili polnjenja in zakaj se ne ujameta

Ko polnite pot, ki se križa ali vsebuje notranjo zanko, izrisovalnik potrebuje pravilo za odločitev, katera območja so znotraj lika in katera so luknje. ISO 32000-1 §8.5.3.3 določa dve pravili, ki lahko isto geometrijo poslikata drugače. Pravilo neničelnega navitja (nonzero winding rule) šteje predznačene prehode žarka, usmerjenega iz testne točke v neskončnost, pri čemer prišteje ena za vsak odsek, ki prečka od leve proti desni, in odšteje ena za vsakega, ki prečka v nasprotno smer; točka je znotraj, ko skupna vsota ni nič. Pravilo sodo-liho (even-odd rule) ne upošteva smeri in preprosto šteje prehode, pri čemer točko označi za notranjo, ko je število liho

Klasičen primer, ko se razhajata, je lik z luknjo, na primer krof ali podložka. Narišite zunanjo mejo in notranjo mejo znotraj nje. V skladu s pravilom sodo-liho notranja zanka vedno izreže luknjo, saj se vsaka točka med obema mejama prečka enkrat, vsaka točka znotraj notranje zanke pa dvakrat. V skladu s pravilom neničelnega navitja se luknja pojavi le, če se notranja zanka vrti v nasprotni smeri kot zunanja; če ju zavrtite v isto smer, se navitja medsebojno ojačajo, namesto da bi se izničila, notranje območje pa se zapolni kot trdno telo. Petkraka zvezda, narisana kot ena sama samosekajoča se obroba, kaže isto delitev: sodo-liho pusti osrednji petkotnik prazen, medtem ko ga neničelno navitje zapolni

PDF Library for Delphi izbere pravilo na podlagi klica, ki ga izvedete za slikanje, in ne na podlagi zastavice. DrawPath polni s pravilom neničelnega navitja; DrawPathEvenOdd polni s pravilom sodo-liho. Obe sprejmeta enak celoštevilski način: 0 izriše le obrobo, 1 le polni in 2 polni ter izriše obrobo. Pravilo sodo-liho je preprostejše orodje za izrezovanje lukenj ravno zato, ker vam ni treba upravljati smeri podpoti

PDF Library for Delphi: gnezdene pravokotnike, napolnjene po pravilih soda-liha in neničelnega ovijanja: soda-liha izdolbe notranji pravokotnik kot luknjo, neničelno ovijanje pa napolni polno, razen če se notranja zanka ovije nasprotno od zunanje
Ista geometrija podložka pod dvema zakonitima odgovoroma — DrawPathEvenOdd ignorira smer ovoja in vedno izdolbe luknjo, DrawPath pa sešteva predznačene ovoje, ki se okrepijo, ko si podpoti delijo smer
// Isti škatli, dve pravili zapolnjevanja, dva različna rezultata
// Navijanje Nonzero: obe škatli se navijata v isti smeri, zato notranja
// NE izreže luknje in celotna zunanja škatla se zapolni
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

// Sodo-liho: notranja škatla je prekrižana s sodim številom prehodov,
// zato skozi zunanjo škatlo izreže čisto pravokotno luknjo
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

Aksialni prelivi spreminjajo barvo vzdolž črte

Enotna barva polnila je ena vrednost na celotnem območju. Preliv neprekinjeno spreminja barvo, najpreprostejša vrsta pa je aksialni oziroma linearni preliv. ISO 32000-1 §8.7.4.5 ga določa kot aksialno senčenje vrste 2 (Type 2 axial shading): podate dve točki, ki določata os, začetno barvo na prvi točki in končno barvo na drugi, izrisovalnik pa interpolira barvo vzdolž te osi. Vsaka točka na zapolnjenem območju prevzame barvo svoje pravokotne projekcije na os, tako da preliv poteka v pasovih pod pravim kotom na črto med obema točkama

V PDF Library for Delphi je preliv poimenovan vir dokumenta, ki ga ustvarite enkrat in ga nato izberete kot aktivno barvo. NewRGBAxialShader ga registrira. Podpis je NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dve končni točki osi, trojice RGB na vsakem koncu kot vrednosti v območju od 0 do 1 ter zastavica Extend. Ko je Extend nastavljen na 1, se končne barve nadaljujejo kot enotno polnilo preko končnih točk osi, kar običajno želite, da koti območja izven osi ne ostanejo neposlikani; 0 jih pusti nedotaknjene. Ko senčilnik obstaja, ga povežete s SetFillShader za zapolnjena območja, SetLineShader za izrisane obrobe ali SetTextShader za besedilo. Povezava ostane aktivna za risalne klice, ki sledijo, tako da pot, ki jo narišete naslednjo, prevzame preliv namesto enotne barve

PDF Library for Delphi: anatomija aksialnega preliva RGB, ki prikazuje barvne trakove pravokotno na os senčenja, projekcijo na os in Extend, ki končni barvi nadaljuje kot polnilo čez končni točki
Osninski senčnik interpolira vzdolž osi med dvema končnima barvama, vsaka točka pa vzorči svojo pravokotno projekcijo na to črto — Extend = 1 pušča končni barvi teči čez končni točki, tako da ostanejo vogali naslikani
// Navpični preliv definiraj enkrat: modra spodaj do bele zgoraj
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // začetna točka in začetni RGB
  0, 260,   1.00, 1.00, 1.00,    // končna točka in končni RGB
  1);                            // 1 = konca podaljšaj kot enotno barvo

// Izberi preliv za polnilo, nato z njim izriši pravokotnik
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = zapolni, zdaj zapolnjeno s shaderjem

Os je tukaj navpična, od y=100 do y=260 pri fiksnem x, so barvni pasovi potekajo vodoravno, pravokotnik pa bledi iz modre barve pri dnu v belo na vrhu. Ker je senčilnik označen z imenom, lahko ena definicija zapolni poljubno število likov na strani, preklop nazaj na enotno barvo pa je le še en klic SetFillColor pred naslednjo potjo

Vzorci tlakovanja ponavljajo celico

Kjer preliv gladko spreminja eno barvo, vzorec tlakovanja ponavlja majhno umetniško delo čez celotno območje. ISO 32000-1 §8.7.3.1 definira vzorec tlakovanja kot celico vzorca (pattern cell), neodvisen del vsebine, ki ga izrisovalnik ponavlja na fiksni mreži, da zapolni območje, ki ga slika. Na ta način zgradite šrafuro za inženirsko polnilo, ponavljajoč se motiv znamke za glavo ali teksturirano ozadje, ki ostane vektorsko ostro in ne tehta skoraj nič, ne glede na to, kako veliko je območje, saj je celica shranjena enkrat in se nanjo sklicuje povsod

PDF Library for Delphi zgradi celico vzorca iz zajete vsebine strani. Stran ali območje zajamete s CapturePage, zajem spremenite v poimenovan vzorec z NewTilingPatternFromCapturedPage(PatternName, CaptureID), nato pa ta vzorec izberete kot trenutno polnilo s SetFillTilingPattern(PatternName). Od takrat naprej je vsaka pot, ki jo zapolnite, poslikana s ponavljajočo se celico namesto z enotno barvo, natanko tako, kot deluje polnjenje s senčilom, vendar s tlakovano celico kot virom barve. Zaporedje je obsežnejše od enega klica, so, če vam korak zajema ni znan, obravnavajte vzorec kot dvostopenjsko operacijo: najprej ustvarite zajeto celico, nato pa jo povežite kot polnilo po imenu, preden narišete območje, ki ga želite tlakovati

Sestavljanje primitivov skupaj

Deli se sestavljajo neposredno. Zapolnjen Bezierjev lik je pot krivulj, poslikana z DrawPath. Isti obris, poslikan z DrawPathEvenOdd po dodajanju notranje zanke, prikazuje luknjo, ki bi jo polnjenje z navitjem zaprlo. Pravokotnik, napolnjen s prelivom, je polje, povezano s senčilnikom. Spodnji primer zaporedno izriše vse tri, tako da je razlika med obema praviloma polnjenja vidna na eni strani, nato pa pod njih položi ploščo s prelivom

// 1. Zapolnjena oblika Bezier (navijanje nonzero)
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. Isti obris z notranjo zanko, zapolnjen po pravilu sodo-liho za prikaz luknje
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);                              // luknja je izrezana

// 3. Pravokotnik, zapolnjen z osnim prelivom
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);

Dve podrobnosti si je vredno zapomniti. Slikarski klic določi pravilo polnjenja, zato je izbira med DrawPath in DrawPathEvenOdd izbira med neničelnim navitjem in sodo-lihim pravilom, pri čemer vam pravilo sodo-liho pri likih z luknjami prihrani razmišljanje o smeri podpoti. Grafično stanje pa se vzorči v trenutku slikanja: nastavite barve, širino črte in povezavo senčilnika pred klicem slikanja, saj je to stanje, ki ga pogon bere. Najprej zgradite, konfigurirajte stanje in na koncu poslikajte – tako se vektorski model vsakič obnaša predvidljivo

Od tu so naravni naslednji koraki branje vektorjev in besedila nazaj iz obstoječega dokumenta, kar je opisano v našem članku o ekstrakciji besedila, slik in pisav, ter izrisovanje enakega risalnega modela v kontekst naprave Windows za predogled na zaslonu in tiskanje, kar je obravnavano v vodiču za tiskanje in predogled. Klici za poti, senčilnike in vzorce, opisani tukaj, so na voljo kot del knjižnice Delphi PDF Library skupaj z vmesniki API za besedilo, slike, obrazce in podpisovanje, ki so obravnavani drugje na tem blogu