Teknisk artikel

Vektorgrafik i PDF med Delphi: banor och övertoningar

Den mesta Delphi-kod som rör (touches) PDF behandlar (treats) formatet som en behållare för två saker: textflöden (runs of text) och några utplacerade bitmappar. Den synen är korrekt så långt den räcker (as far as it goes), och den lämnar den mest kapabla (capable) delen av formatet oanvänd (unused). En PDF-sida är en upplösnings-oberoende 2D-canvas (2D canvas) byggd på samma avbildnings-modell (imaging model) som PostScript. Den kan rita linjer, kurvor, fyllda regioner, övertoningar (gradients) och upprepande mönster, allt som vektorer (vectors) som förblir skarpa vid vilken zoom som helst och skrivs ut (print) med enhetens fulla upplösning. Om du ritar en logotyp, ett diagram, en vattenstämpel eller en certifikat-kant (certificate border), är vektor-banan (vector path) nästan alltid rätt primitiv (primitive), och den är mindre och krispigare än den rastrerade bilden många program sträcker sig efter i stället

Den här artikeln går igenom (walks through) vektor-modellen så som ISO 32000-1 definierar den och visar de matchande PDFlibPas-anropen. Syftet (aim) är att göra specifikationen konkret, eftersom (because) API:et mappar tätt mot (onto) den, och att förstå det ena lär dig det andra

Sidan är en ban-maskin (path machine)

ISO 32000-1 §8.5 beskriver (describes) grafik i två faser som aldrig överlappar (overlap). Först konstruerar du en bana (path), vilken är ren geometri (pure geometry) utan något synligt resultat. Sedan (Then) målar du (paint) den banan i en enda operation som drar upp dess kontur (strokes its outline), fyller (fills) dess inre (interior) eller gör bådadera. Ingenting dyker upp (appears) på sidan under konstruktionen. Banan är en abstrakt sekvens av punkter och segment som hålls (held) i grafik-tillståndet (graphics state) tills en målar-operator (painting operator) konsumerar den (consumes it), vid vilken punkt den renderas (rendered) och kastas bort (discarded)

En bana är gjord (made) av en (one) eller flera (more) delbanor (subpaths). En delbana börjar vid en punkt och växer (grows) genom (by) att lägga till (appending) segment: raka linjer, kubiska Bezier-kurvor, och på vissa plattformar hela rektanglar (rectangles) tillagda som en egen stängd delbana (closed subpath). I PDFlibPas öppnar du en bana med StartPath, vilket sätter startpunkten (starting point), sedan förlänger du den (extend it) med AddLineToPath och AddCurveToPath. Varje anrop flyttar fram (advances) en implicit aktuell punkt (current point), så att nästa segment fortsätter från (from) där (where) det senaste slutade. ClosePath ritar ett sista, rakt segment tillbaka till delbanans start, vilket spelar roll för konturritning (stroking) eftersom (because) det producerar en riktig linjekoppling (line join) vid den stängande hjäss-punkten (closing vertex) i stället för två lösa änd-kåpor (end caps)

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

Kurvor använder AddCurveToPath, vilket tar två Bezier-kontrollpunkter och en slutpunkt: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Kurvan löper (runs) från den aktuella punkten till (EndX, EndY), dragen mot (pulled toward) de två kontrollpunkterna längs (along) vägen. Cirkulära bågar (Circular arcs) är tillgängliga (available) genom AddArcToPath(CenterX, CenterY, TotalAngle), där radien hämtas (taken) från avståndet mellan den aktuella punkten och mitten (center), och motorn (engine) skickar ut (emits) bågen (arc) som en kedja av Bezier-segment. Rektanglar har en genväg (shortcut), AddBoxToPath(Left, Top, Width, Height), vilken lägger till en komplett (complete) stängd (closed) rektangel som en egen delbana (subpath) utan (without) en föregående StartPath

Två fyllningsregler (fill rules), och varför de är oense (disagree)

När du fyller (fill) en bana som korsar sig själv (crosses itself) eller innehåller (contains) en inre (inner) ögla (loop), behöver renderaren (renderer) en regel för att besluta (deciding) vilka regioner som är inuti figuren (shape) och vilka som är hål. ISO 32000-1 §8.5.3.3 definierar två (two), och de kan måla (paint) samma geometri (geometry) annorlunda (differently). Nollskild-lindnings-regeln (nonzero winding rule) räknar de teckenförsedda korsningarna (signed crossings) av en stråle (ray) kastad (cast) från en testpunkt till oändligheten (infinity), adderar (adding) ett (one) för varje (each) segment som korsar från vänster till höger (left to right) och subtraherar (subtracting) ett för varje som korsar på andra hållet (the other way); punkten är inuti (inside) när totalsumman inte är (is not) noll (zero). Jämn-udda-regeln (even-odd rule) ignorerar (ignores) riktning (direction) och räknar (counts) helt enkelt korsningar, och kallar punkten inuti (inside) när antalet (count) är udda (odd)

Det klassiska (classic) fallet där (where) de divergerar (diverge) är (is) en form med ett hål, en munk (donut) eller en bricka (washer). Rita (Draw) en yttre (outer) gräns (boundary) och (and) en inre (inner) gräns inuti (inside) den (it). Under jämn-udda-regeln skär (carves out) den inre öglan alltid (always) ut (out) ett hål (hole), eftersom varje (any) punkt mellan (between) de två (two) gränserna korsas (is crossed) en gång (once) och varje punkt inuti den inre öglan korsas två gånger. Under nollskild-lindnings-regeln framträder (appears) hålet (hole) endast (only) om (if) den inre öglan lindar sig (winds) i motsatt (opposite) riktning till (to) den yttre; linda (wind) dem åt samma håll (same way) och lindningarna (windings) förstärks (reinforce) i stället för att (instead of) ta ut varandra (canceling), och den inre regionen fylls (fills) massivt (solid). En femuddig stjärna ritad som en enda, självkorsande (self-intersecting) kontur (outline) visar samma (same) klyvning (split): jämn-udda (even-odd) lämnar (leaves) den centrala (central) femhörningen (pentagon) tom (empty) medan (while) nollskild lindning fyller den

PDFlibPas väljer (selects) regeln (rule) via det anrop (call) du gör för att (to) måla (paint), inte via (by) en (a) flagga (flag). DrawPath fyller (fills) med nollskild-lindnings-regeln; DrawPathEvenOdd fyller med jämn-udda-regeln. Båda tar (take) samma (same) heltals-läge (integer mode): 0 drar enbart upp (strokes only) konturen (outline), 1 fyller enbart (fills only), och 2 fyller och drar upp kontur (strokes). Jämn-udda-regeln är det enklare verktyget (tool) för utstansade (punch-out) hål (holes) just (precisely) eftersom den inte (does not) kräver (require) att du hanterar (manage) delbanans (subpath) riktning

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

Axiella övertoningar varierar (vary) färg längs en linje

En platt (flat) fyllningsfärg (fill color) är ett värde över hela (across the whole) regionen. En övertoning (gradient) varierar färgen kontinuerligt, och den enklaste sorten är den axiella (axial), eller linjära (linear), övertoningen. ISO 32000-1 §8.7.4.5 specificerar (specifies) den som en Typ 2-axiell-skuggning (Type 2 axial shading): du ger två punkter som definierar (define) en (an) axel (axis), en startfärg (start color) vid den första punkten och en slutfärg (end color) vid den andra, och renderaren interpolerar (interpolates) färgen längs (along) den där axeln. Varje punkt i den fyllda regionen tar (takes) färgen från sin vinkelräta projektion (perpendicular projection) på (onto) axeln, så (so) övertoningen löper (runs) i band i räta vinklar (right angles) mot linjen mellan de två punkterna

I PDFlibPas är (is) en övertoning en namngiven (named) dokument-resurs (document resource) du (you) skapar (create) en gång (once) och sedan (then) väljer (select) som den aktiva målarfärgen (paint). NewRGBAxialShader registrerar den (registers it). Signaturen är NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): de två axel-ändpunkterna (axis endpoints), RGB-tripplarna (triples) vid (at) varje (each) ände som (as) värden (values) i intervallet (range) 0 till 1, och en Extend-flagga. Med Extend satt (set) till 1 fortsätter (carry on) slut-färgerna som (as) massiv fyllning (solid fill) bortom (beyond) axel-ändpunkterna, vilket är vad du oftast vill ha så att hörnen på (of) en region utanför axeln (outside the axis) inte (are not) lämnas (left) omålade (unpainted); 0 lämnar dem orörda (untouched). Så snart (Once) shadern (shader) existerar binder (bind) du den (it) med (with) SetFillShader för fyllda regioner, SetLineShader för ritade konturer (stroked outlines), eller (or) SetTextShader för text. Bindningen (binding) förblir aktiv för de ritningsanrop (drawing calls) som följer (follow), så banan (path) du (you) målar (paint) härnäst (next) tar (takes) övertoningen i stället för (instead of) en platt (flat) färg (color)

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

Axeln (axis) här (here) är vertikal, från y=100 till y=260 vid (at) ett fast (fixed) x, så färgbanden (color bands) löper horisontellt och rektangeln bleknar (fades) från blå (blue) vid sin (its) bas till vit (white) vid (at) dess topp (top). Eftersom (Because) shadern (shader) nycklas (keyed) via namn, kan en definition fylla ett godtyckligt antal (any number of) former (shapes) på (on) sidan, och (and) att (switching) växla tillbaka till (to) en platt färg är bara (just) ytterligare (another) ett (a) SetFillColor-anrop (call) före (before) nästa bana

Rutnätsmönster (Tiling patterns) upprepar (repeat) en cell

Där (Where) en övertoning (gradient) varierar (varies) en (a) enda (single) färg smidigt (smoothly), upprepar (repeats) ett rutnätsmönster (tiling pattern) en liten (small) bit av ett (a) konstverk (artwork) tvärs över (across) en region. ISO 32000-1 §8.7.3.1 definierar ett rutnätsmönster som (as) en (a) mönster-cell (pattern cell), en oberoende (independent) bit innehåll (content), som renderaren (renderer) replikerar (replicates) på ett fast rutnät (fixed grid) för att (to) täcka (tile) den area (area) som målas (being painted). Det här är hur du bygger streckningar (hatching) för en teknisk (engineering) fyllning (fill), ett (a) upprepande varumärkes-motiv (brand motif) bakom en (a) sidhuvud (header), eller en texturerad bakgrund som förblir (stays) vektor-skarp (vector-sharp) och inte (almost) väger någonting (weighs nothing) oavsett (no matter) hur stor (large) arean (area) är, eftersom cellen lagras (stored) en gång och (and) refereras överallt (everywhere)

PDFlibPas bygger (builds) mönstercellen från (from) fångat sidinnehåll (captured page content). Du fångar en sida eller en region med CapturePage, förvandlar (turn) fångsten (capture) till (into) ett namngivet (named) mönster med (with) NewTilingPatternFromCapturedPage(PatternName, CaptureID), och väljer (select) sedan (then) det mönstret som (as) den nuvarande (current) fyllningen med SetFillTilingPattern(PatternName). Från den (that) punkten och framåt (on), målas varje (any) bana du (you) fyller (fill) med den (the) upprepande cellen snarare än en (a) platt färg, exakt (exactly) som en shader-fyllning fungerar (works) men med en celliserad cell (tiled cell) som målarfärgskälla (paint source). Sekvensen (sequence) är mer (more) involverad (involved) än (than) ett enda (single) anrop, så (so) om fångst-steget (capture step) är obekant (unfamiliar), behandla (treat) mönstret (pattern) som en (a) tvåstegs-operation (two-stage operation): producera den fångade cellen först (first), bind (bind) den sedan som (as) en fyllning (fill) med namn innan du ritar (drawing) den region (region) du vill (want) ha täckt med mönstret (tiled)

Att sätta (Putting) ihop (together) primitiverna (primitives)

Bitarna (pieces) bygger ihop sig (compose) direkt (directly). En (A) fylld Bezier-blob (blob) är en bana av (of) kurvor målad med DrawPath. Samma (The same) kontur målad med (with) DrawPathEvenOdd efter att ha lagt till (adding) en inre (inner) ögla visar (shows) ett hål som nollskild-lindnings-fyllningen skulle (would) ha (have) stängt (closed). En (A) övertonings-fylld rektangel är (is) en låda (box) bunden (bound) till en (a) shader. Exemplet (example) nedan ritar (draws) alla tre (three) i sekvens (sequence) så att skillnaden (difference) mellan (between) de (the) två (two) fyllningsreglerna är (is) synlig (visible) på en (one) sida, och lägger sedan (then lays) en (a) övertonings-panel under (beneath) dem (them)

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

Två (Two) detaljer (details) är värda att (worth) hålla fast vid (holding onto). Målar-anropet beslutar (decides) fyllningsregeln (fill rule), så (so) valet mellan (between) DrawPath och DrawPathEvenOdd är valet mellan nollskild lindning (nonzero winding) och jämn-udda (even-odd), och för (for) former med hål besparar (spares) jämn-udda-regeln dig (you) från (from) att resonera (reasoning) kring delbanans riktning. Och (And) grafik-tillståndet samplas (is sampled) vid (at) det (the) ögonblick du målar: sätt (set) dina färger, linjebredd (line width) och (and) shader-bindning innan (before) målar-anropet (painting call), eftersom (because) det (that) är tillståndet motorn (engine) läser. Konstruera först (first), konfigurera (configure) tillståndet (state), måla sist (last), och vektor-modellen beter sig (behaves) förutsägbart (predictably) varje gång (every time)

Härifrån är (are) de (the) naturliga nästa stegen (steps) att läsa (reading) vektorer (vectors) och text tillbaka ut ur (out of) ett (an) befintligt dokument, vilket (which) täcks (covered) i (in) vår artikel om extraktion av text, bilder och teckensnitt, och (and) att rendera (rendering) samma (same) rit-modell (drawing model) till en Windows device context för (for) förhandsgranskning på skärm (on-screen preview) och utskrift, täckt (covered) i (in) genomgången om utskrift och förhandsgranskning (print and preview walkthrough). Ban-, shader- och mönster-anropen som beskrivs här skeppas som en del av Delphi PDF Library tillsammans med (alongside) de text-, bild-, formulär- och signerings-API:er (APIs) som täcks (covered) på andra håll på den här bloggen