Den mesta Delphi-kod som hanterar PDF behandlar formatet som en behållare för två saker: textsekvenser och några placerade bitmappar. Den synen är korrekt så långt den går, men den lämnar den mest kapabla delen av formatet oanvänd. En PDF-sida är en upplösningsoberoende 2D-ritbana byggd på samma bildmodell som PostScript. Den kan rita linjer, kurvor, fyllda regioner, gradienter och upprepade mönster, allt som vektorer som förblir skarpa vid valfri zoomning och skrivs ut med enhetens fulla upplösning. Om du ritar en logotyp, ett diagram, en vattenstämpel eller en certifikatram är vektorbanan nästan alltid den rätta primitiven, och den är mindre och skarpare än den rastrerade bild som många program använder istället.
Denna artikel går igenom vektormodellen så som ISO 32000-1 definierar den och visar de matchande PDFlibPas-anropen. Syftet är att göra specifikationen konkret, eftersom API:et mappar nära mot den, och att förstå den ena lär dig den andra.
Sidan är en banmaskin
ISO 32000-1 §8.5 beskriver grafik i två faser som aldrig överlappar. Först konstruerar du en bana (path), vilket är ren geometri utan synligt resultat. Sedan ritar (paint) du den banan i en enda operation som antingen drar dess konturlinje (stroke), fyller dess inre (fill), eller gör både och. Ingenting visas på sidan under konstruktionen. Banan är en abstrakt sekvens av punkter och segment som hålls i grafikläget (graphics state) tills en ritoperator konsumerar den, varvid den renderas och slängs.
En bana består av en eller flera underbanor (subpaths). En underbana börjar vid en punkt och växer genom att lägga till segment: raka linjer, kubiska Bezier-kurvor och på vissa plattformar hela rektanglar som läggs till som egna stängda underbanor. I PDFlibPas öppnar du en bana med StartPath, vilket sätter startpunkten, och utökar den sedan med AddLineToPath och AddCurveToPath. Varje anrop flyttar fram en implicit aktuell punkt, så att nästa segment fortsätter där det förra slutade. ClosePath ritar ett sista rakt segment tillbaka till underbanans början, vilket är viktigt för konturlinjer eftersom det skapar en riktig linjesammanfogning vid det stängande hörnet istället för två lösa ändar.
// 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, som tar två Bezier-styrpunkter och en slutpunkt: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Kurvan löper från den aktuella punkten till (EndX, EndY), dragen mot de två styrpunkterna längs vägen. Cirkulära bågar är tillgängliga via AddArcToPath(CenterX, CenterY, TotalAngle), där radien tas från avståndet mellan den aktuella punkten och mitten, och motorn skickar ut bågen som en kedja av Bezier-segment. Rektanglar har en genväg, AddBoxToPath(Left, Top, Width, Height), som lägger till en komplett stängd rektangel som en egen underbana utan en föregående StartPath.
Två fyllnadsregler och varför de inte är överens
När du fyller en bana som korsar sig själv eller innehåller en inre loop, behöver renderaren en regel för att avgöra vilka regioner som är innanför formen och vilka som är hål. ISO 32000-1 §8.5.3.3 definierar två, och de kan rita samma geometri på olika sätt. Regeln för lindningsnummer som inte är noll (nonzero winding rule) räknar de tecknade korsningarna av en stråle dragen från en testpunkt till oändligheten, adderar ett för varje segment som korsar från vänster till höger och subtraherar ett för varje som korsar åt andra hållet; punkten är innanför när summan inte är noll. Even-odd-regeln ignorerar riktningen och räknar helt enkelt korsningar, och betraktar punkten som innanför när antalet är udda.
Det klassiska fallet där de skiljer sig åt är en form med ett hål, en munk eller en bricka. Rita en yttre gräns och en inre gräns inuti den. Under even-odd-regeln skapar den inre loopen alltid ett hål, eftersom varje punkt mellan de två gränserna korsas en gång och varje punkt innanför den inre loopen korsas två gånger. Under nonzero winding-regeln visas hålet endast om den inre loopen vindlar i motsatt riktning mot den yttre; vindlar de åt samma håll förstärks vindlingarna istället för att ta ut varandra, och den inre regionen fylls helt. En femuddig stjärna ritad som en enda själv-korsande kontur visar samma uppdelning: even-odd lämnar den centrala femhörningen tom medan nonzero winding fyller den.
PDFlibPas väljer regeln utifrån det anrop du gör för att rita, inte via en flagga. DrawPath fyller med nonzero winding-regeln; DrawPathEvenOdd fyller med even-odd-regeln. Båda tar samma heltalsläge: 0 ritar endast konturen (stroke), 1 fyller endast (fill), och 2 fyller och ritar konturen. Even-odd-regeln är det enklare verktyget för att stansa ut hål just eftersom den inte kräver att du hanterar underbanans 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 gradienter varierar färg längs en linje
En platt fyllnadsfärg har ett och samma värde över hela regionen. En gradient varierar färgen kontinuerligt, och den enklaste typen är den axiella, eller linjära, gradienten. ISO 32000-1 §8.7.4.5 specificerar den som en Type 2 axial shading: du anger två punkter som definierar en axel, en startfärg vid den första punkten och en slutfärg vid den andra, och renderaren interpolerar färgen längs den axeln. Varje punkt i den fyllda regionen tar färgen från sin vinkelräta projektion på axeln, så gradienten löper i band vinkelrätt mot linjen mellan de två punkterna.
I PDFlibPas är en gradient en namngiven dokumentresurs som du skapar en gång och sedan väljer som aktiv färg. NewRGBAxialShader registrerar den. Signaturen är NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): de två axeländpunkterna, RGB-tripplarna vid varje ände som värden i intervallet 0 till 1, och en Extend-flagga. Med Extend satt till 1 fortsätter ändfärgerna som solid fyllning bortom axelns ändpunkter, vilket är vad du vanligtvis vill ha så att hörnen av en region utanför axeln inte lämnas omålade; 0 lämnar dem orörda. När shadern väl existerar binder du den med SetFillShader för fyllda regioner, SetLineShader för ritade konturer eller SetTextShader för text. Bindningen förblir aktiv för de ritningsanrop som följer, så att banan du ritar härnäst får gradienten istället för en platt färg.
// 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 här är vertikal, från y=100 till y=260 vid ett fast x, så färgbanden löper horisontellt och rektangeln tonar från blått vid sin bas till vitt vid sin topp. Eftersom shadern refereras med namn kan en definition fylla valfritt antal former på sidan, och att växla tillbaka till en platt färg är bara ytterligare ett SetFillColor-anrop före nästa bana.
Tiling patterns upprepar en cell
Där en gradient varierar en enskild färg mjukt, upprepar ett upprepningsmönster (tiling pattern) en liten del av ett konstverk över en region. ISO 32000-1 §8.7.3.1 definierar ett tiling-mönster som en mönstercell, en oberoende del av innehållet, som renderaren duplicerar på ett fast rutnät för att fylla området som målas. Det är så du bygger skraffering (hatching) för teknisk fyllning, ett upprepande varumärkesmotiv bakom ett sidhuvud eller en texturerad bakgrund som förblir vektorskarp och nästan inte väger någonting oavsett hur stort området är, eftersom cellen lagras en gång och refereras överallt.
PDFlibPas bygger mönstercellen från fångat sidinnehåll. Du fångar en sida eller en region med CapturePage, gör om fångsten till ett namngivet mönster med NewTilingPatternFromCapturedPage(PatternName, CaptureID), och väljer sedan det mönstret som den aktuella fyllningen med SetFillTilingPattern(PatternName). Från den tidpunkten målas varje bana du fyller med den upprepande cellen istället för en platt färg, exakt som en shader-fyllning fungerar men med en mönstrad cell som källa. Sekvensen är mer komplicerad än ett enskilt anrop, så om fångststeget är obekant kan du behandla mönstret som en tvåstegsoperation: generera den fångade cellen först, och bind den sedan som en fyllning via namn innan du ritar det område du vill mönstra.
Att lägga samman primitiverna
Delarna kan kombineras direkt. En fylld Bezier-form är en bana av kurvor som ritas med DrawPath. Samma kontur ritad med DrawPathEvenOdd efter att ha lagt till en inre loop visar ett hål som winding-fyllningen skulle ha stängt. En gradientfylld rektangel är en ruta bunden till en shader. Exemplet nedan ritar alla tre i sekvens så att skillnaden mellan de två fyllnadsreglerna är synlig på en och samma sida, och lägger sedan en 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);
Två detaljer är värda att komma ihåg. Ritningsanropet avgör fyllnadsregeln, så valet mellan DrawPath och DrawPathEvenOdd är valet mellan nonzero winding och even-odd, och för former med hål besparar even-odd-regeln dig från att behöva tänka på underbanans riktning. Och grafikläget (graphics state) samplas i det ögonblick du ritar: ställ in dina färger, linjebredd och shader-bindning före ritningsanropet, eftersom det är det läget som motorn läser. Konstruera först, konfigurera läget sedan, rita sist, så fungerar vektormodellen förutsägbart varje gång.
Härifrån är de naturliga nästa stegen att läsa tillbaka vektorer och text ur ett befintligt dokument, vilket täcks i vår artikel om text-, bild- och typsnittsextraktion, samt att rendera samma ritmodell till en Windows-enhetskontext (device context) för förhandsgranskning på skärmen och utskrift, vilket täcks i genomgången av utskrift och förhandsgranskning. Anropen för banor, shaders och mönster som beskrivs här levereras som en del av Delphi PDF Library tillsammans med API:erna för text, bilder, formulär och signering som beskrivs på andra ställen i den här bloggen.