Technical Article

Vektorska grafika u PDF-u s Delphiem: Staze i gradijenti

Većina Delphi koda koji dodiruje PDF tretira taj format kao spremnik za dvije stvari: dijelove teksta i nekoliko postavljenih bitmapa. Taj je pogled točan u onoj mjeri u kojoj ide, ali ostavlja najsposobniji dio formata neiskorištenim. PDF stranica je 2D platno neovisno o razlučivosti izgrađeno na istom slikovnom modelu kao i PostScript. Može crtati linije, krivulje, ispunjena područja, gradijente i ponavljajuće uzorke, sve kao vektore koji ostaju oštri pri svakom zumiranju i ispisuju se u punoj razlučivosti uređaja. Ako crtate logotip, grafikon, vodeni žig ili obrub certifikata, vektorska staza je gotovo uvijek pravi primitiv, te je manja i oštrija od rasterizirane slike za kojom mnogi programi umjesto toga posežu.

Ovaj članak prolazi kroz vektorski model kako ga definira ISO 32000-1 i prikazuje odgovarajuće PDFlibPas pozive. Cilj je učiniti specifikaciju konkretnom, jer se API usko preslikava na nju, a razumijevanje jednog uči vas drugom.

Stranica je stroj za staze

ISO 32000-1 §8.5 opisuje grafiku u dvije faze koje se nikada ne preklapaju. Prvo konstruirate stazu, što je čista geometrija bez vidljivog rezultata. Zatim iscrtavate tu stazu u jednoj operaciji koja uokviruje njezin obris, ispunjava njezinu unutrašnjost ili radi oboje. Ništa se ne pojavljuje na stranici tijekom konstrukcije. Staza je apstraktni niz točaka i segmenata koji se drže u grafičkom stanju sve dok je operator crtanja ne potroši, u kojem trenutku se iscrtava i odbacuje.

Staza se sastoji od jedne ili više podstaza. Podstaza počinje u jednoj točki i raste dodavanjem segmenata: ravnih linija, kubičnih Bezierovih krivulja i, na nekim platformama, cijelih pravokutnika dodanih kao vlastita zatvorena podstaza. U PDFlibPas-u stazu otvarate pomoću StartPath, što postavlja početnu točku, a zatim je proširujete pomoću AddLineToPath i AddCurveToPath. Svaki poziv pomiče implicitnu trenutnu točku, tako da sljedeći segment nastavlja od mjesta gdje je prethodni završio. ClosePath crta završni ravni segment natrag do početka podstaze, što je važno za uokvirivanje jer proizvodi stvarni spoj linija na završnom vrhu umjesto dva labava 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

Krivulje koriste AddCurveToPath, koja uzima dvije Bezierove kontrolne točke i krajnju točku: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Krivulja se proteže od trenutne točke do (EndX, EndY), privučena prema dvjema kontrolnim točkama usput. Kružni lukovi su dostupni putem AddArcToPath(CenterX, CenterY, TotalAngle), gdje se polumjer uzima iz udaljenosti između trenutne točke i središta, a motor emitira luk kao lanac Bezierovih segmenata. Pravokutnici imaju kraticu, AddBoxToPath(Left, Top, Width, Height), koja dodaje potpuni zatvoreni pravokutnik kao vlastitu podstazu bez prethodnog poziva StartPath.

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

Kada ispunjavate stazu koja siječe samu sebe ili sadrži unutarnju petlju, renderer treba pravilo za odlučivanje koja su područja unutar oblika, a koja su rupe. ISO 32000-1 §8.5.3.3 definira dva pravila, i ona mogu istu geometriju obojiti različito. Pravilo nenultog namotaja (nonzero winding rule) broji predznačene prijelaze zrake bačene iz testne točke u beskonačnost, dodajući jedan za svaki segment koji prelazi s lijeva na desno i oduzimajući jedan za svaki koji prelazi obrnuto; točka je unutra kada zbroj nije nula. Parno-neparno pravilo (even-odd rule) zanemaruje smjer i jednostavno broji prijelaze, smatrajući točku unutrašnjom kada je broj neparan.

Klasičan slučaj u kojem se razilaze je oblik s rupom, poput krafne ili podloške. Nacrtajte vanjsku granicu i unutarnju granicu unutar nje. Prema parno-neparnom pravilu, unutarnja petlja uvijek izrezuje rupu, jer se svaka točka između dviju granica prelazi jednom, a svaka točka unutar unutarnje petlje prelazi dvaput. Prema pravilu nenultog namotaja, rupa se pojavljuje samo ako se unutarnja petlja namotava u suprotnom smjeru od vanjske; ako ih namotate u istom smjeru, namotaji se pojačavaju umjesto da se poništavaju, a unutarnje područje se ispunjava kao čvrsto tijelo. Petokraka zvijezda nacrtana kao jedan samoispresecajući obris pokazuje istu podjelu: parno-neparno pravilo ostavlja središnji peterokut praznim, dok ga pravilo nenultog namotaja ispunjava.

PDFlibPas odabire pravilo prema pozivu koji uputite za iscrtavanje, a ne prema zastavici. DrawPath ispunjava prema pravilu nenultog namotaja; DrawPathEvenOdd ispunjava prema parno-neparnom pravilu. Oba uzimaju isti cjelobrojni način rada: 0 iscrtava samo obris, 1 samo ispunjava, a 2 ispunjava i iscrtava obris. Parno-neparno pravilo je jednostavniji alat za izrezivanje rupa upravo zato što ne zahtijeva da upravljate smjerom podstaze.

// 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 mijenjaju boju duž linije

Jednolična boja ispune je jedna vrijednost na cijelom području. Gradijent kontinuirano mijenja boju, a najjednostavnija vrsta je aksijalni ili linearni gradijent. ISO 32000-1 §8.7.4.5 to specificira kao sjenčanje aksijalnog tipa 2 (Type 2 axial shading): zadajete dvije točke koje definiraju os, početnu boju u prvoj točki i krajnju boju u drugoj, a renderer interpolira boju duž te osi. Svaka točka u ispunjenom području poprima boju svoje okomite projekcije na os, pa se gradijent odvija u trakama pod pravim kutom na liniju između dviju točaka.

U PDFlibPas-u gradijent je imenovani resurs dokumenta koji stvarate jednom, a zatim ga odabirete kao aktivnu boju. NewRGBAxialShader ga registrira. Potpis je NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): dvije krajnje točke osi, RGB trojke na svakom kraju kao vrijednosti u rasponu od 0 do 1 i zastavica Extend. S Extend postavljenim na 1, krajnje boje se nastavljaju kao čvrsta ispuna izvan krajnjih točaka osi, što je ono što obično želite kako kutovi područja izvan osi ne bi ostali neobojeni; 0 ih ostavlja netaknutima. Nakon što sjenčanje (shader) postoji, povezujete ga pomoću SetFillShader za ispunjena područja, SetLineShader za uokvirene obrise ili SetTextShader za tekst. Povezivanje ostaje aktivno za pozive crtanja koji slijede, pa staza koju sljedeću iscrtate poprima gradijent umjesto jednolične 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

Os je ovdje okomita, od y=100 do y=260 pri fiksnom x, pa se trake boja protežu vodoravno, a pravokutnik blijedi od plave na bazi do bijele na vrhu. Budući da je sjenčanje (shader) označeno imenom, jedna definicija može ispuniti bilo koji broj oblika na stranici, a povratak na jednoličnu boju je samo još jedan poziv SetFillColor prije sljedeće staze.

Uzorci popločavanja ponavljaju ćeliju

Tamo gdje gradijent glatko mijenja jednu boju, uzorak popločavanja (tiling pattern) ponavlja mali dio grafike preko nekog područja. ISO 32000-1 §8.7.3.1 definira uzorak popločavanja kao ćeliju uzorka (pattern cell), nezavisni dio sadržaja, koji renderer replicira na fiksnoj mreži kako bi popločao područje koje se boja. Na ovaj način gradite šrafiranje za inženjersku ispunu, ponavljajući motiv brenda iza zaglavlja ili teksturiranu pozadinu koja ostaje vektorski oštra i ne teži gotovo ništa bez obzira na to koliko je područje veliko, jer je ćelija pohranjena jednom i referencirana svugdje.

PDFlibPas gradi ćeliju uzorka iz snimljenog sadržaja stranice. Snimite stranicu ili područje pomoću CapturePage, pretvorite snimku u imenovani uzorak pomoću NewTilingPatternFromCapturedPage(PatternName, CaptureID), a zatim odaberete taj uzorak kao trenutnu ispunu pomoću SetFillTilingPattern(PatternName). Od tog trenutka, svaka staza koju ispunite obojana je ponavljajućom ćelijom umjesto jednoličnom bojom, točno onako kako radi ispunjavanje sjenčanjem, ali s popločanom ćelijom kao izvorom boje. Slijed je složeniji od jednog poziva, pa ako vam je korak snimanja nepoznat, tretirajte uzorak kao operaciju u dvije faze: prvo stvorite snimljenu ćeliju, a zatim je povežite kao ispunu po nazivu prije crtanja područja koje želite popločati.

Spajanje primitiva zajedno

Dijelovi se izravno slažu. Ispunjen Bezierov oblik je staza krivulja iscrtana pomoću DrawPath. Isti obris iscrtan pomoću DrawPathEvenOdd nakon dodavanja unutarnje petlje prikazuje rupu koju bi ispuna namotaja zatvorila. Pravokutnik ispunjen gradijentom je okvir povezan sa sjenčanjem. Primjer u nastavku crta sva tri dijela u nizu kako bi razlika između dvaju pravila ispunjavanja bila vidljiva na jednoj stranici, a zatim ispod njih postavlja ploču s gradijentom.

// 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 vrijedi zapamtiti. Poziv za crtanje odlučuje o pravilu ispunjavanja, tako da je izbor između DrawPath i DrawPathEvenOdd zapravo izbor između pravila nenultog namotaja i parno-neparnog pravila, a za oblike s rupama parno-neparno pravilo vas pošteđuje razmišljanja o smjeru podstaze. Grafičko stanje se uzorkuje u trenutku kada crtate: postavite svoje boje, širinu linije i povezivanje sjenčanja (shader) prije poziva crtanja, jer je to stanje koje motor čita. Prvo konstruirajte, konfigurirajte stanje, a na kraju crtajte, i vektorski model će se ponašati predvidljivo svaki put.

Odavde, prirodni sljedeći koraci su čitanje vektora i teksta natrag iz postojećeg dokumenta, što je pokriveno u našem članku o ekstrakciji teksta, slika i fontova, te iscrtavanje istog modela crtanja na kontekst Windows uređaja za pregled na zaslonu i ispis, pokriveno u vodiču za ispis i pregled. Pozivi staza, sjenčanja i uzoraka opisani ovdje isporučuju se kao dio Delphi PDF knjižnice uz API-je za tekst, slike, obrasce i potpise koji su pokriveni drugdje na ovom blogu.