Tehnički članak

Vektorska grafika u PDF-u sa Delphi-jem: Putanje i gradijenti

Većina Delphi koda koji radi sa PDF-om tretira taj format kao kontejner za dve stvari: delove teksta i nekoliko postavljenih bitmapa. To viđenje je tačno koliko i nepotpuno, i ostavlja najmoćniji deo formata neiskorišćenim. PDF stranica je 2D kanvas nezavisan od rezolucije, izgrađen na istom modelu vizuelizacije kao i PostScript. Može da crta linije, krive, popunjene regione, gradijente i ponavljajuće šablone, i to sve kao vektore koji ostaju oštri pri svakom zumiranju i štampaju se u punoj rezoluciji uređaja. Ako crtate logo, grafikon, vodeni žig ili ivicu sertifikata, vektorska putanja je skoro uvek pravi primitiv, i manja je i oštrija od rasterizovane slike za kojom mnogi programi umesto toga posežu

Ovaj članak prolazi kroz vektorski model onako kako ga definiše standard ISO 32000-1 i prikazuje odgovarajuće PDF Library for Delphi pozive. Cilj je da se specifikacija učini konkretnom, jer se API blisko preslikava na nju, a razumevanje jednog vas uči drugom

Stranica je mašina za putanje

ISO 32000-1 §8.5 opisuje grafiku u dve faze koje se nikada ne preklapaju. Prvo konstruišete putanju, što je čista geometrija bez vidljivog rezultata. Zatim oslikavate tu putanju u jednoj operaciji koja iscrtava njenu konturu, popunjava njenu unutrašnjost ili radi oba. Tokom konstrukcije se ništa ne pojavljuje na stranici. Putanja je apstraktni niz tačaka i segmenata koji se drže u grafičkom stanju sve dok je operater slikanja ne potroši, u kom trenutku se ona renderuje i odbacuje

PDF Library for Delphi: Dijagram životnog ciklusa PDF putanje koji pokazuje izgradnju kao nevidljivu čistu geometriju, praćenu paint operatorom koji iscrtava, puni ili oba, pa troši i odbacuje putanju
Izgradnja četvorougla ne boji ništa — poziv DrawPath čita grafičko stanje jednom, prikaže potez, ispunu ili oboje, i baca putanju

Putanja se sastoji od jedne ili više podputanja. Podputanja počinje u jednoj tački i raste dodavanjem segmenata: pravih linija, kubnih Bezijeovih krivih, a na nekim platformama i celih pravougaonika koji se dodaju kao sopstvena zatvorena podputanja. U PDF Library for Delphi-u otvarate putanju sa StartPath, što postavlja početnu tačku, a zatim je proširujete sa AddLineToPath i AddCurveToPath. Svaki poziv pomera implicitnu trenutnu tačku unapred, tako da se sledeći segment nastavlja tamo gde se prethodni završio. ClosePath povlači završni pravolinijski segment nazad do početka podputanje, što je važno za iscrtavanje konture jer stvara pravi spoj linija na završnom temenu umesto dva nepovezana kraja

// Zatvoreni četvorougao, prvo ocrtan pa popunjen
PDF.SetLineColor(0, 0, 0);
PDF.SetFillColor(0.6, 0.8, 1.0);
PDF.SetLineWidth(1.5);

PDF.StartPath(150, 100);           // otvori putanju na prvom temenu
PDF.AddLineToPath(220, 140);
PDF.AddLineToPath(180, 210);
PDF.AddLineToPath(110, 170);
PDF.ClosePath;                     // pravi segment nazad do (150, 100)
PDF.DrawPath(2);                   // 2 = popuna i kontura; putanja je potrošena

Krive koriste AddCurveToPath, koji uzima dve Bezijeove kontrolne tačke i krajnju tačku: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Kriva se kreće od trenutne tačke do (EndX, EndY), povučena prema dvema kontrolnim tačkama duž puta. Kružni lukovi su dostupni preko AddArcToPath(CenterX, CenterY, TotalAngle), gde se poluprečnik uzima iz udaljenosti između trenutne tačke i centra, a mehanizam emituje luk kao lanac Bezijeovih segmenata. Pravougaonici imaju prečicu, AddBoxToPath(Left, Top, Width, Height), koja dodaje kompletan zatvoreni pravougaonik kao sopstvenu podputanju bez prethodnog poziva StartPath

Dva pravila popunjavanja i zašto se ne slažu

Kada popunjavate putanju koja seče samu sebe ili sadrži unutrašnju petlju, rendereru je potrebno pravilo da odluči koji regioni su unutar oblika, a koji predstavljaju rupe. ISO 32000-1 §8.5.3.3 definiše dva pravila, i ona mogu istu geometriju oslikati drugačije. Pravilo ne-nultog namotaja broji označene preseke zraka povučenog od test tačke do beskonačnosti, dodajući jedan za svaki segment koji prelazi sleva nadesno i oduzimajući jedan za svaki koji prelazi obrnuto; tačka je unutra kada ukupan zbir nije nula. Pravilo par-nepar ignoriše smer i jednostavno broji preseke, proglašavajući tačku unutrašnjom kada je broj preseka neparan

Klasičan slučaj gde se ona razilaze jeste oblik sa rupom, poput krofne ili podloške. Nacrtajte spoljnu granicu i unutrašnju granicu unutar nje. Prema pravilu par-nepar, unutrašnja petlja uvek izrezuje rupu, jer se svaka tačka između dve granice preseca jednom, a svaka tačka unutar unutrašnje petlje dva puta. Prema pravilu ne-nultog namotaja, rupa se pojavljuje samo ako se unutrašnja petlja namotava u suprotnom smeru od spoljne; ako ih namotate u istom smeru, namotaji se pojačavaju umesto da se poništavaju, i unutrašnji region se popunjava u potpunosti. Petokraka zvezda nacrtana kao jedna samopresecajuća kontura pokazuje istu podelu: par-nepar ostavlja centralni petougao praznim, dok ga ne-nulti namotaj popunjava

PDF Library for Delphi bira pravilo na osnovu poziva koji uputite za slikanje, a ne preko zastavice. DrawPath popunjava po pravilu ne-nultog namotaja; DrawPathEvenOdd popunjava po pravilu par-nepar. Oba uzimaju isti celobrojni režim: 0 iscrtava samo konturu, 1 samo popunjava, a 2 popunjava i iscrtava konturu. Pravilo par-nepar je lakša alatka za izrezivanje rupa upravo zato što ne zahteva da upravljate smerom podputanje

PDF Library for Delphi: Ugnježđeni pravougaonici popunjeni pod even-odd i nonzero winding pravilima: even-odd izdubuje unutrašnji pravougaonik kao rupu dok nonzero winding puni čvrsto osim ako se unutrašnja petlja vije suprotno od spoljašnje
Ista geometrija podloške pod dva legalna odgovora — DrawPathEvenOdd ignoriše smer obuhvatanja i uvek iseče rupu, dok DrawPath sabira potpisane obuhvate koji se pojačavaju kad podputanje dele smer
// Ista dva okvira, dva pravila popune, dva različita rezultata
// Nonzero winding: oba okvira imaju isti smer, pa unutrašnji
// NE izrezuje rupu i ceo spoljašnji okvir se popunjava jednolično
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: unutrašnji okvir se preseče paran broj puta,
// pa probija čistu pravougaonu rupu kroz spoljašnji okvir
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

Aksijalni gradijenti menjaju boju duž linije

Jednobojno popunjavanje je jedna vrednost preko celog regiona. Gradijent neprekidno menja boju, a najjednostavnija vrsta je aksijalni, odnosno linearni gradijent. Standard ISO 32000-1 §8.7.4.5 ga specifikuje kao aksijalno senčenje tipa 2: dajete dve tačke koje definišu osu, početnu boju u prvoj tački i krajnju boju u drugoj, a renderer interpolira boju duž te ose. Svaka tačka u popunjenom regionu poprima boju svoje normalne projekcije na osu, tako da se gradijent kreće u trakama pod pravim uglom u odnosu na liniju između te dve tačke

U PDF Library for Delphi-u, gradijent je imenovani resurs dokumenta koji kreirate jednom, a zatim birate kao aktivnu boju. NewRGBAxialShader ga registruje. Potpis je NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dve krajnje tačke ose, RGB trojke na svakom kraju kao vrednosti u opsegu od 0 do 1, i zastavica Extend. Sa parametrom Extend postavljenim na 1, krajnje boje se nastavljaju kao čvrsto popunjavanje izvan krajnjih tačaka ose, što je obično ono što želite kako uglovi regiona izvan ose ne bi ostali neobojeni; 0 ih ostavlja netaknutim. Kada senčenje postoji, povezujete ga sa SetFillShader za popunjene regione, SetLineShader za iscrtane konture ili SetTextShader za tekst. Povezivanje ostaje aktivno za crtačke pozive koji slede, tako da putanja koju sledeću oslikate poprima gradijent umesto ravne boje

PDF Library for Delphi: Anatomija aksijalnog RGB gradijenta koja pokazuje trake boja upravne na shading osu, projekciju na osu, i Extend koji krajnje boje nastavlja kao čvrsto punjenje iza krajnjih tačaka
Aksijalni šejder interpolira duž ose između dve krajnje boje, svaka tačka uzorkuje svoju upravnu projekciju na tu liniju — Extend = 1 drži krajnje boje da teku iza krajnjih tačaka pa uglovi ostaju obojeni
// Jednom definiši vertikalni gradijent: plavo na dnu do belog na vrhu
PDF.NewRGBAxialShader('panelGrad',
  0, 100,   0.10, 0.25, 0.55,    // početna tačka i početni RGB
  0, 260,   1.00, 1.00, 1.00,    // krajnja tačka i krajnji RGB
  1);                            // 1 = proširi krajeve kao punu boju

// Izaberi gradijent kao popunu, pa njime iscrtaj pravougaonik
PDF.SetFillShader('panelGrad');
PDF.AddBoxToPath(80, 100, 300, 160);
PDF.DrawPath(1);                 // 1 = popuna, sada popunjeno shaderom

Osa je ovde vertikalna, od y=100 do y=260 pri fiksnom x, tako da se trake boje kreću horizontalno i pravougaonik bledi od plave u dnu do bele na vrhu. Pošto je senčenje identifikovano po imenu, jedna definicija može popuniti bilo koji broj oblika na stranici, a povratak na jednobojno popunjavanje je samo još jedan poziv SetFillColor pre sledeće putanje

Ponavljajući šabloni ponavljaju ćeliju

Tamo gde gradijent glatko menja jednu boju, ponavljajući šablon ponavlja mali deo grafike preko regiona. ISO 32000-1 §8.7.3.1 definiše ponavljajući šablon kao ćeliju šablona, nezavisan deo sadržaja koji renderer replicira na fiksnoj mreži kako bi popunio oblast koja se oslikava. Ovako gradite šrafuru za inženjersko popunjavanje, ponavljajući motiv brenda iza zaglavlja ili teksturiranu pozadinu koja ostaje vektorski oštra i ne teži skoro ništa bez obzira na veličinu oblasti, jer se ćelija čuva jednom, a referencira svuda

PDF Library for Delphi gradi ćeliju šablona iz snimljenog sadržaja stranice. Snimite stranicu ili region sa CapturePage, pretvarate snimak u imenovani šablon sa NewTilingPatternFromCapturedPage(PatternName, CaptureID), a zatim birate taj šablon kao trenutno popunjavanje sa SetFillTilingPattern(PatternName). Od tog trenutka, svaka putanja koju popunite oslikava se ponavljajućom ćelijom umesto jednobojno, tačno onako kako radi popunjavanje senčenjem, ali sa popločanom ćelijom kao izvorom boje. Redosled je složeniji od jednog poziva, pa ako vam je korak snimanja nepoznat, tretirajte šablon kao dvofaznu operaciju: prvo napravite snimljenu ćeliju, a zatim je povežite kao popunjavanje po imenu pre crtanja regiona koji želite da popločate

Spajanje primitiva

Delovi se direktno kombinuju. Popunjena Bezijeova mrlja je putanja kriva oslikana sa DrawPath. Isti obris oslikan sa DrawPathEvenOdd nakon dodavanja unutrašnje petlje prikazuje rupu koju bi popunjavanje namotajem zatvorilo. Pravougaonik popunjen gradijentom je okvir povezan sa senčenjem. Primer ispod crta sva tri segmenta redom tako da je razlika između dva pravila popunjavanja vidljiva na jednoj stranici, a zatim postavlja panel sa gradijentom ispod njih

// 1. Popunjen Bezierov oblik (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. Isti obris sa unutrašnjom petljom, popunjen pravilom even-odd da prikaže rupu
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);                              // rupa je izrezana

// 3. Pravougaonik popunjen aksijalnim gradijentom
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);

Dva detalja vredi zapamtiti. Poziv za slikanje odlučuje o pravilu popunjavanja, tako da je izbor između DrawPath i DrawPathEvenOdd zapravo izbor između ne-nultog namotaja i pravila par-nepar, a za oblike sa rupama pravilo par-nepar vas pošteđuje razmišljanja o smeru podputanje. Takođe, grafičko stanje se uzorkuje u trenutku slikanja: podesite boje, širinu linije i povezivanje senčenja pre poziva za slikanje, jer je to stanje koje mehanizam čita. Prvo konstruišite, zatim konfigurišite stanje i na kraju slikajte, i vektorski model će se ponašati predvidivo svaki put

Odavde, prirodni sledeći koraci su čitanje vektora i teksta nazad iz postojećeg dokumenta, što je pokriveno u našem članku o ekstrakciji teksta, slika i fontova, i renderovanje istog modela crtanja na Windows kontekst uređaja za pregled na ekranu i štampanje, što je pokriveno u vodiču kroz štampanje i pregled. Pozivi putanja, senčenja i šablona koji su ovde opisani isporučuju se kao deo Delphi PDF biblioteke zajedno sa API-jima za tekst, slike, obrasce i potpisivanje o kojima se govori na drugim mestima na ovom blogu