Technical Article

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 PDFlibPas 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.

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 PDFlibPas-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.

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

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.

PDFlibPas 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.

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

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 PDFlibPas-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.

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

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.

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

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.