Technical Article

Vektorgrafik i PDF med Delphi: Stier og gradienter

De fleste Delphi-koder, der berører PDF, behandler formatet som en beholder for to ting: tekstblokke og nogle få placerede bitmaps. Det synspunkt er korrekt så langt det rækker, men det efterlader den mest kapable del af formatet ubrugt. En PDF-side er et opløsningsuafhængigt 2D-lærred bygget på samme billedmodel som PostScript. Det kan tegne linjer, kurver, udfyldte områder, gradienter og gentagne mønstre, alt sammen som vektorer, der forbliver skarpe ved enhver zoom og udskrives i enhedens fulde opløsning. Hvis du tegner et logo, et diagram, et vandmærke eller en certifikatramme, er vektorstien næsten altid den rigtige primitiv, og den er mindre og skarpere end det rasteriserede billede, som mange programmer vælger i stedet.

Denne artikel gennemgår vektormodellen, som ISO 32000-1 definerer den, og viser de matchende PDFlibPas-kald. Målet er at gøre specifikationen konkret, fordi API'en afspejler den nøje, og forståelse af den ene lærer dig om den anden.

Siden is en stimaskine

ISO 32000-1 §8.5 beskriver grafik i to faser, der aldrig overlapper. Først konstruerer du en sti, hvilket er ren geometri uden synligt resultat. Derefter tegner (paint) du stien i en enkelt operation, der tegner dens omrids (stroke), udfylder dens indre (fill) eller gør begge dele. Intet vises på siden under konstruktionen. Stien er en abstrakt sekvens af punkter og segmenter, der holdes i grafikstatus (graphics state), indtil en tegneoperator forbruger den, hvorefter den renderes og kasseres.

En sti er bygget af en eller flere understier (subpaths). En understi begynder ved et punkt og vokser ved at tilføje segmenter: rette linjer, kubiske Bezier-kurver og på nogle platforme hele rektangler tilføjet som deres egen lukkede understi. I PDFlibPas åbner du en sti med StartPath, som sætter startpunktet, og udvider den derefter med AddLineToPath og AddCurveToPath. Hvert kald flytter et implicit aktuelt punkt frem, så det næste segment fortsætter, hvor det sidste sluttede. ClosePath tegner et afsluttende lige segment tilbage til understiens start, hvilket er vigtigt for stregtegning (stroke), fordi det producerer en rigtig linjesamling ved det lukkede hjørne i stedet for to løse ender.

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

Kurver bruger AddCurveToPath, som tager to Bezier-kontrolpunkter og et slutpunkt: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Kurven løber fra det aktuelle punkt til (EndX, EndY), trukket mod de to kontrolpunkter undervejs. Cirkulære buer er tilgængelige via AddArcToPath(CenterX, CenterY, TotalAngle), hvor radius tages fra afstanden mellem det aktuelle punkt og centrum, og motoren udsender buen som en kæde af Bezier-segmenter. Rektangler har en genvej, AddBoxToPath(Left, Top, Width, Height), som tilføjer et komplet lukket rektangel som sin egen understi uden en forudgående StartPath.

To udfyldningsregler, og hvorfor de er uenige

Når du udfylder en sti, der krydser sig selv eller indeholder en indre løkke, har rendereren brug for en regel til at afgøre, hvilke områder der er inden i formen, og hvilke der er huller. ISO 32000-1 §8.5.3.3 definerer to regler, og de kan tegne den samme geometri forskelligt. Nonzero winding-reglen tæller de fortegnede krydsninger af en stråle sendt fra et testpunkt til uendeligt, idet der lægges én til for hvert segment, der krydser fra venstre mod højre, og trækkes én fra for hvert segment, der krydser den anden vej; punktet er indenfor, når totalen ikke er nul. Even-odd-reglen ignorerer retningen og tæller blot krydsninger, idet punktet kaldes indenfor, når optællingen er ulige.

Det klassiske tilfælde, hvor de divergerer, er en form med et hul, som en donut eller en spændeskive. Tegn en ydre grænse og en indre grænse inden i den. Under even-odd-reglen udskærer den indre løkke altid et hul, fordi ethvert punkt mellem de to grænser krydses én gang, og ethvert punkt inden i den indre løkke krydses to gange. Under nonzero winding-reglen vises hullet kun, hvis den indre løkke snor sig i den modsatte retning af den ydre; hvis de snor sig samme vej, forstærker snoningene hinanden i stedet for at ophæve hinanden, og det indre område udfyldes helt. En femtakket stjerne tegnet som et enkelt selvoverskærende omrids viser samme opdeling: even-odd efterlader den centrale femkant tom, mens nonzero winding udfylder den.

PDFlibPas vælger reglen ud fra det kald, du foretager for at tegne, ikke ud fra et flag. DrawPath udfylder med nonzero winding-reglen; DrawPathEvenOdd udfylder med even-odd-reglen. Begge tager samme heltals-tilstand: 0 tegner kun omridset (stroke), 1 udfylder kun (fill), og 2 udfylder og tegner omrids. Even-odd-reglen er det nemmeste værktøj til udstansede huller, netop fordi den ikke kræver, at du styrer understiens retning.

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

Aksiale gradienter varierer farven langs en linje

En flad udfyldningsfarve har én værdi over hele området. A gradient varierer farven kontinuerligt, og den enkleste type er den aksiale eller lineære gradient. ISO 32000-1 §8.7.4.5 specificerer den som en Type 2 aksial skyggelægning (axial shading): du angiver to punkter, der definerer en akse, en startfarve ved det første punkt og en slutfarve ved det andet, og rendereren interpolerer farven langs den pågældende akse. Hvert punkt i det udfyldte område får farven fra dets vinkelrette projektion på aksen, så gradienten forløber i bånd vinkelret på linjen mellem de to punkter.

I PDFlibPas er en gradient er en navngiven dokumentressource, du opretter én gang og derefter vælger som den aktive maling. NewRGBAxialShader registrerer den. Signaturen er NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): de to akse-endepunkter, RGB-triplerne i hver ende som værdier i intervallet 0 til 1, og et Extend-flag. Med Extend sat til 1 fortsætter endefarverne som solid udfyldning ud over aksens endepunkter, hvilket normalt er det, man ønsker, så hjørnerne af et område uden for aksen ikke efterlades umalede; 0 efterlader dem urørte. Når shaderen eksisterer, binder du den med SetFillShader for udfyldte områder, SetLineShader for stregomrids eller SetTextShader for tekst. Bindingen forbliver aktiv for de efterfølgende tegnekald, så den sti, du tegner næste gang, får gradienten i stedet for en flad farve.

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

Aksen her er lodret, fra y=100 til y=260 ved en fast x, så farvebåndene løber vandret, og rektanglet toner ud fra blåt i bunden til hvidt i toppen. Fordi shaderen er identificeret ved navn, kan én definition udfylde et vilkårligt antal former på siden, og at skifte tilbage til en flad farve er blot et nyt SetFillColor-kald før den næste sti.

Flisemønstre gentager en celle

Hvor en gradient varierer en enkelt farve glat, gentager et flisemønster (tiling pattern) et lille stykke grafik hen over et område. ISO 32000-1 §8.7.3.1 definerer et flisemønster som en mønstercelle, et uafhængigt stykke indhold, som rendereren replikerer på et fast gitter for at dække det område, der tegnes. Det er sådan, du bygger skravering til en teknisk udfyldning, et gentaget brandmotiv bag et sidehoved eller en tekstureret baggrund, der forbliver skarpt som vektor og næsten intet vejer uanset hvor stort området er, fordi cellen gemmes én gang og refereres overalt.

PDFlibPas opretter mønstercellen ud fra optaget sideindhold. Du optager en side eller et område med CapturePage, omdanner optagelsen til et navngivet mønster med NewTilingPatternFromCapturedPage(PatternName, CaptureID) og vælger derefter dette mønster som den aktuelle udfyldning med SetFillTilingPattern(PatternName). Fra det tidspunkt tegnes enhver sti, du udfylder, med den gentagne celle i stedet for en flad farve, præcis som en shader-udfyldning fungerer, men med en flisebelagt celle som farvekilde. Sekvensen er mere kompliceret end et enkelt kald, så hvis optagelsestrinnet er ukendt, kan du behandle mønsteret som en to-trins operation: Opret den optagne celle først, og bind den derefter som en udfyldning ved navn, før du tegner det område, du vil have dækket.

Sammensætning af primitiverne

Delene kan sammensættes direkte. En udfyldt Bezier-klat er en sti af kurver tegnet med DrawPath. Det samme omrids tegnet med DrawPathEvenOdd efter tilføjelse af en indre løkke viser et hul, som winding-udfyldningen ville have lukket. Et gradient-udfyldt rektangel er en boks bundet til en shader. Eksemplet nedenfor tegner alle tre i rækkefølge, så forskellen mellem de to udfyldningsregler er synlig på én side, og lægger derefter et gradientpanel under dem.

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

To detaljer er værd at huske på. Tegnekaldet bestemmer udfyldningsreglen, så valget mellem DrawPath og DrawPathEvenOdd er valget mellem nonzero winding og even-odd, og for former med huller sparer even-odd-reglen dig for at spekulere på understiens retning. Og grafikstatusen samples i det øjeblik, du tegner: Indstil dine farver, linjebredde og shader-binding før tegnekaldet, fordi det er den status, motoren læser. Konstruer først, konfigurer statusen, tegn til sidst, og vektormodellen opfører sig forudsigeligt hver gang.

Herfra er de naturlige næste skridt at læse vektorer og tekst tilbage fra et eksisterende dokument, hvilket er dækket i vores artikel om tekst-, billed- og skrifttypeudtrækning, samt at renderere samme tegnemodel to en enhedskontekst til visning på skærmen og udskrivning, dækket i gennemgangen af print og preview. Kaldene til stier, shadere og mønstre, der beskrives her, leveres som en del af Delphi PDF Library sammen med API'erne til tekst, billeder, formularer og signaturer, som er dækket andre steder på denne blog.