Suurin osa PDF-tiedostoja käsittelevästä Delphi-koodista kohtelee tiedostomuotoa vain kahden asian säiliönä: tekstikatkelmina ja muutamina sijoitettuina bittikarttoina. Tämä näkemys on oikea niin pitkälle kuin se uluttuu, mutta se jättää tiedostomuodon kyvykkäimmän osan käyttämättä. PDF-sivu on resoluutiosta riippumaton 2D-kangas, joka perustuu samaan kuvantamismalliin kuin PostScript. Se voi piirtää viivoja, käyriä, täytettyjä alueita, gradientteja ja toistuvia kuvioita – kaikki vektoreina, jotka pysyvät terävinä millä tahansa tarkennuksella ja tulostuvat laitteen täydellä resoluutiolla. Jos piirrät logon, kaavion, vesileiman tai todistuksen kehyksen, vektoripolku on lähes aina oikea peruselementti, ja se on pienempi ja terävämpi kuin rasteroitu kuva, johon monet ohjelmat sen sijaan tukeutuvat.
Tässä artikkelissa käydään läpi vektorimalli sellaisena kuin ISO 32000-1 sen määrittelee ja näytetään vastaavat PDFlibPas-kutsut. Tavoitteena on tehdä määrittelystä konkreettinen, koska API vastaa sitä läheisesti, ja toisen ymmärtäminen opettaa toisen.
Sivu on polkukone
ISO 32000-1 §8.5 kuvaa grafiikkaa kahdessa vaiheessa, jotka eivät koskaan mene päällekkäin. Ensin rakennetaan polku, mikä on puhdasta geometriaa ilman näkyvää tulosta. Sitten polku maalataan yhdellä operaatiolla, joka piirtää sen ääriviivat, täyttää sen sisäosan tai tekee molemmat. Mitään ei ilmesty sivulle rakennusvaiheessa. Polku on abstrakti pisteiden ja segmenttien sarja, jota pidetään grafiikkatilassa, kunnes maalausoperaattori kuluttaa sen, jolloin se renderöidään ja hävitetään.
Polku koostuu yhdestä tai useammasta alipolusta. Alipolku alkaa pisteestä ja kasvaa lisäämällä segmenttejä: suoria viivoja, kuutio-Bezier-käyriä ja joillakin alustoilla kokonaisia suorakulmioita, jotka lisätään omina suljettuina alipolkuinaan. PDFlibPas-ohjelmassa polku avataan StartPath-kutsulla, joka asettaa aloituspisteen, ja sitä jatketaan AddLineToPath- ja AddCurveToPath-kutsuilla. Jokainen kutsu siirtää implisiittistä nykyistä pistettä, joten seuraava segmentti jatkuu siitä, mihin edellinen päättyi. ClosePath piirtää viimeisen suoran segmentin takaisin alipolun alkuun, millä on merkitystä viivan piirtämisessä, koska se tuottaa todellisen viivan liitoksen sulkevaan kärkeen kahden irrallisen päätykannen 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 AddCurveToPath-kutsua, joka ottaa kaksi Bezier-ohjauspistettä ja päätepisteen: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Käyrä kulkee nykyisestä pisteestä pisteeseen (EndX, EndY), vetäytyen matkalla kohti kahta ohjauspistettä. Ympyränkaaria on saatavana AddArcToPath(CenterX, CenterY, TotalAngle)-kutsun kautta, jossa säde otetaan nykyisen pisteen ja keskipisteen välisestä etäisyydestä, ja moottori tuottaa kaaren Bezier-segmenttien ketjuna. Suorakulmioilla on oikotie AddBoxToPath(Left, Top, Width, Height), joka lisää täydellisen suljetun suorakulmion omana alipolkunaan ilman edeltävää StartPath-kutsua.
Kaksi täyttösääntöä 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 päättääkseen, mitkä alueet ovat muodon sisällä ja mitkä ovat reikiä. ISO 32000-1 §8.5.3.3 määrittelee kaksi sääntöä, ja ne voivat maalata saman geometrian eri tavalla. Nonzero winding -sääntö laskee testipisteestä äärettömyyteen heitetyn säteen etumerkilliset risteykset lisäten yhden jokaisesta segmentistä, joka risteää vasemmalta oikealle, ja vähentäen yhden jokaisesta, joka risteää toisinpäin; piste on sisällä, kun summa ei ole nolla. Even-odd-sääntö jättää suunnan huomiotta ja laskee yksinkertaisesti risteykset kutsuen pistettä sisäpuoliseksi, kun määrä on pariton.
Klassinen tapaus, jossa ne eroavat, on muoto, jossa on reikä, kuten donitsi tai aluslevy. Piirrä ulkoreuna ja sisäreuna sen sisään. Even-odd-säännön mukaan sisäsilmukka leikkaa aina reiän, koska mikä tahansa piste kahden reunan välillä ylittyy kerran ja mikä tahansa piste sisäsilmukan sisällä ylittyy kahdesti. Nonzero winding -säännön mukaan reikä tulee näkyviin vain, jos sisäsilmukka kiertää vastakkaiseen suuntaan kuin ulompi; jos ne kiertävät samaan suuntaan, kierrokset vahvistavat toisiaan peruutuksen sijaan, ja sisäalue täyttyy yhtenäiseksi. Viisisakarainen tähti, joka on piirretty yhtenä itseään leikkaavana ääriviivana, näyttää saman jaon: even-odd jättää keskimmäisen viisikulmion tyhjäksi, kun taas nonzero winding täyttää sen.
PDFlibPas valitsee säännön maalauskutsun mukaan, ei lipulla. DrawPath täyttää nonzero winding -säännöllä; DrawPathEvenOdd täyttää even-odd-säännöllä. Molemmat ottavat saman kokonaislukutilan: 0 piirtää vain ääriviivat, 1 täyttää vain ja 2 täyttää ja piirtää ääriviivat. Even-odd-sääntö on helpompi työkalu reikien tekemiseen juuri siksi, että se ei vaadi alipolun suunnan hallintaa.
// 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 gradientit vaihtelevat väriä linjaa pitkin
Akaat täyttöväri on yksi arvo koko alueella. Gradientti vaihtaa väriä jatkuvasti, ja yksinkertaisin tyyppi on aksiaalinen eli lineaarinen gradientti. ISO 32000-1 §8.7.4.5 määrittelee sen tyypin 2 aksiaalisena varjostuksena: annat kaksi pistettä, jotka määrittelevät akselin, aloitusvärin ensimmäisessä pisteessä ja loppuvärin toisessa pisteessä, ja renderöijä interpoloi värin kyseistä akselia pitkin. Jokainen täytetyn alueen piste ottaa värin sen kohtisuorasta projektiosta akselille, joten gradientti kulkee vyöhykkeinä suorassa kulmassa kahden pisteen väliseen viivaan nähden.
PDFlibPas-ohjelmassa gradientti on nimetty dokumenttiresurssi, jonka luot kerran ja valitset sitten aktiiviseksi maaliksi. NewRGBAxialShader rekisteröi sen. Allekirjoitus on NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): kaksi akselin päätepistettä, RGB-kolmikot kussakin päässä arvoina välillä 0–1 ja Extend-lippu. Kun Extend on asetettu arvoon 1, päätevärit jatkuvat yhtenäisenä täyttönä akselin päätepisteiden ulkopuolella, mikä on yleensä toivottavaa, jotta akselin ulkopuolisen alueen kulmat eivät jää maalaamatta; 0 jättää ne koskemattomiksi. Kun varjostin on olemassa, sidot sen SetFillShader-kutsulla täytetyille alueille, SetLineShader-kutsulla viivoille tai SetTextShader-kutsulla tekstille. Sidos pysyy aktiivisena seuraavissa piirustuskutsuissa, joten seuraavaksi maalaamasi polku ottaa gradientin 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
Akseli tässä on pystysuora, y=100 - y=260 kiinteällä x-arvolla, joten värivyöhykkeet kulkevat vaakasuorassa ja suorakulmio haalistuu alaosan sinisestä yläosan valkoiseksi. Koska shader tunnistetaan nimellä, yksi määrittely voi täyttää minkä tahansa määrän muotoja sivulla, ja palaaminen tasaiseen väriin on vain toinen SetFillColor-kutsu ennen seuraavaa polkua.
Toistuvat kuviot monistavat solua
Siinä missä gradientti vaihtaa yksittäistä väriä tasaisesti, toistuva kuvio toistaa pientä taideteosta alueen yli. ISO 32000-1 §8.7.3.1 määrittelee tiling-kuvion kuvioluonnokseksi, itsenäiseksi sisällöksi, jonka renderöijä monistaa kiinteään ruudukkoon täytettävän alueen peittämiseksi. Näin rakennat viivoituksen teknistä täyttöä varten, toistuvan brändiaiheen otsikon taakse tai teksturoidun taustan, joka pysyy vektoriterävänä ja painaa tuskin mitään riippumatta alueen koosta, koska solu tai kuvio tallennetaan kerran ja siihen viitataan kaikkialla.
PDFlibPas rakentaa kuvioluonnoksen kaapatusta sivun sisällöstä. Kaappaat sivun tai alueen CapturePage-kutsulla, muutat kaappauksen nimetyksi kuvioksi NewTilingPatternFromCapturedPage(PatternName, CaptureID)-kutsulla ja valitset sitten kyseisen kuvion nykyiseksi täytöksi SetFillTilingPattern(PatternName)-kutsulla. Tästä eteenpäin kaikki täyttämäsi polut maalataan toistuvalla solulla tasaisen värin sijaan, täsmälleen kuten shader-täyttö toimii, mutta kuvioluonnoksen ollessa maalin lähde. Jakso on monimutkaisempi kuin yksi kutsu, joten jos kaappausvaihe ei ole tuttu, käsittele kuviota kaksivaiheisena operaationa: tuota ensin kaapattu solu ja sido se sitten täytöksi nimen perusteella ennen täytettävän alueen piirtämistä.
Putting the primitives together
Osat yhdistyvät suoraan. Täytetty Bezier-muoto on käyrien polku, joka on maalattu DrawPath-kutsulla. Sama ääriviiva maalattuna DrawPathEvenOdd-kutsulla sisäisen silmukan lisäämisen jälkeen näyttää reiän, jonka nonzero-täyttö olisi sulkenut. Gradientilla täytetty suorakulmio on laatikko, joka on sidottu shaderiin. Alla oleva esimerkki piirtää kaikki kolme peräkkäin, jotta kahden täyttösäännön ero näkyy yhdellä sivulla, ja asettaa sitten gradienttipaneelin 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 päättää täyttösäännön, joten valinta DrawPath- ja DrawPathEvenOdd-kutsun välillä on valinta nonzero winding - ja even-odd-säännön välillä, ja reikiä sisältäville muodoille even-odd-sääntö säästää sinut alipolun suunnan pohtimiselta. Ja grafiikkatila näytteistetään maalaushetkellä: aseta värit, viivan leveys ja shader-sidos ennen maalauskutsua, koska se on tila, jonka moottori lukee. Rakenna ensin, määritä tila, maalaa viimeisenä, ja vektorimalli toimii ennakoitavasti joka kerta.
Tästä luonnolliset seuraavat vaiheet ovat vektoreiden ja tekstin lukeminen takaisin olemassa olevasta dokumentista, mikä käsitellään artikkelissamme tekstin, kuvan ja fontin purkamisesta, ja saman piirustusmallin renderöinti Windows-laitekontekstiin näytön esikatselua ja tulostusta varten, mikä käsitellään tulostuksen ja esikatselun ohjeessa. Tässä kuvatut polku-, shader- ja kuvio-kutsut toimitetaan osana Delphin PDF-kirjastoa teksti-, kuva-, lomake- ja allekirjoitus-API:en rinnalla, joita käsitellään muualla tässä blogissa.