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