Technical Article

Grafică vectorială în PDF cu Delphi: Căi și gradienți

Cea mai mare parte a codului Delphi care interacționează cu PDF tratează formatul ca pe un container pentru două lucruri: blocuri de text și câteva imagini bitmap plasate. Această viziune este corectă în felul ei și lasă neutilizată cea mai capabilă parte a formatului. O pagină PDF este o pânză 2D independentă de rezoluție, construită pe același model de imagine ca și PostScript. Poate desena linii, curbe, regiuni umplute, gradienți și modele repetitive, toate ca vectori care rămân clari la orice zoom și se tipăresc la rezoluția maximă a dispozitivului. Dacă desenați un logo, o diagramă, un filigran sau o bordură de certificat, calea vectorială este aproape întotdeauna primitiva potrivită și este mai mică și mai clară decât imaginea rasterizată pe care o folosesc în schimb multe programe.

Acest articol parcurge modelul vectorial așa cum îl definește ISO 32000-1 și arată apelurile PDFlibPas corespunzătoare. Scopul este de a face specificația concretă, deoarece API-ul se mapează strâns pe aceasta, iar înțelegerea uneia vă învață și despre cealaltă.

Pagina este o mașină de căi

ISO 32000-1 §8.5 descrie grafica în două faze care nu se suprapun niciodată. Mai întâi construiți o cale, care este geometrie pură, fără un rezultat vizibil. Apoi pictați acea cale într-o singură operație care îi trasează conturul (stroke), îi umple interiorul (fill) sau le face pe amândouă. Nimic nu apare pe pagină în timpul construcției. Calea este o secvență abstractă de puncte și segmente păstrate în starea grafică până când un operator de desenare o consumă, moment în care este redată și eliminată.

O cale este formată din una sau mai multe sub-căi. O sub-cale începe la un punct și crește prin adăugarea de segmente: linii drepte, curbe Bezier cubice și, pe unele platforme, dreptunghiuri întregi adăugate ca propria lor sub-cale închisă. În PDFlibPas deschideți o cale cu StartPath, care setează punctul de pornire, apoi o extindeți cu AddLineToPath și AddCurveToPath. Fiecare apel avansează un punct curent implicit, astfel încât segmentul următor continuă de unde s-a terminat ultimul. ClosePath desenează un segment drept final înapoi la începutul sub-căii, ceea ce contează pentru trasare, deoarece produce o îmbinare reală a liniilor la vârful de închidere, în loc de două capete libere.

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

Curbele folosesc AddCurveToPath, care preia două puncte de control Bezier și un punct final: AddCurveToPath(CtAX, CtAY, CtBX, CtBY, EndX, EndY). Curba rulează de la punctul curent la (EndX, EndY), fiind atrasă spre cele două puncte de control pe parcurs. Arcele circulare sunt disponibile prin AddArcToPath(CenterX, CenterY, TotalAngle), unde raza este preluată din distanța dintre punctul curent și centru, iar motorul emite arcul ca un lanț de segmente Bezier. Dreptunghiurile au o scurtătură, AddBoxToPath(Left, Top, Width, Height), care adaugă un dreptunghi închis complet ca propria sa sub-cale, fără un StartPath precedent.

Două reguli de umplere și de ce nu sunt de acord

Atunci când umpleți o cale care se intersectează sau conține o buclă interioară, redatorul are nevoie de o regulă pentru a decide care regiuni sunt în interiorul formei și care sunt goluri. ISO 32000-1 §8.5.3.3 definește două, iar acestea pot colora diferit aceeași geometrie. Regula de înfășurare diferită de zero (nonzero winding rule) numără trecerile cu semn ale unei raze proiectate de la un punct de testare la infinit, adăugând unu pentru fiecare segment care traversează de la stânga la dreapta și scăzând unu pentru fiecare care traversează invers; punctul este în interior când totalul nu este zero. Regula par-impar (even-odd rule) ignoră direcția și numără pur și simplu trecerile, considerând punctul în interior atunci când numărul este impar.

Cazul clasic în care acestea diverg este o formă cu un gol, o gogoașă sau o șaibă. Desenați o graniță exterioară și o graniță interioară în interiorul ei. Sub regula par-impar, bucla interioară decupează întotdeauna un gol, deoarece orice punct dintre cele două granițe este traversat o singură dată și orice punct din interiorul buclei interioare este traversat de două ori. Sub regula de înfășurare diferită de zero, golul apare numai dacă bucla interioară se înfășoară în direcția opusă celei exterioare; înfășurați-le în aceeași direcție și înfășurările se întăresc în loc să se anuleze, iar regiunea interioară se umple complet. O stea cu cinci colțuri desenată ca un singur contur auto-intersectat arată aceeași diferență: par-impar lasă pentagonul central gol, în timp ce înfășurarea diferită de zero îl umple.

PDFlibPas selectează regula prin apelul pe care îl faceți pentru desenare, nu printr-un flag. DrawPath umple cu regula de înfășurare diferită de zero; DrawPathEvenOdd umple cu regula par-impar. Ambele preiau același mod întreg: 0 desenează doar conturul, 1 doar umple și 2 umple și desenează conturul. Regula par-impar este instrumentul mai simplu pentru golurile decupate tocmai pentru că nu vă cere să gestionați direcția sub-căii.

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

Gradienții axiali variază culoarea de-a lungul unei linii

O culoare de umplere plană reprezintă o singură valoare pe întreaga regiune. Un gradient variază culoarea continuu, iar cel mai simplu tip este gradientul axial sau liniar. ISO 32000-1 §8.7.4.5 îl specifică ca o umbrire axială de tip 2 (Type 2 axial shading): oferiți două puncte care definesc o axă, o culoare de început la primul punct și o culoare de sfârșit la al doilea, iar redatorul interpolează culoarea de-a lungul acelei axe. Fiecare punct din regiunea umplută preia culoarea proiecției safe perpendiculare pe axă, astfel încât gradientul rulează în benzi în unghi drept față de linia dintre cele două puncte.

În PDFlibPas, un gradient este o resursă de document numită pe care o creați o singură dată și apoi o selectați ca desen activ. NewRGBAxialShader o înregistrează. Semnătura este NewRGBAxialShader(ShaderName, StartX, StartY, StartRed, StartGreen, StartBlue, EndX, EndY, EndRed, EndGreen, EndBlue, Extend): cele două puncte finale ale axei, tripletele RGB la fiecare capăt ca valori în intervalul de la 0 la 1 și un flag Extend. Cu Extend setat la 1, culorile de capăt continuă ca umplere solidă dincolo de punctele finale ale axei, ceea ce doriți de obicei pentru ca colțurile unei regiuni din afara axei să nu rămână nepictate; 0 le lasă neatinse. Odată ce shader-ul există, îl asociați cu SetFillShader pentru regiunile umplute, SetLineShader pentru contururile trasate sau SetTextShader pentru text. Asocierea rămâne activă pentru apelurile de desenare care urmează, astfel încât calea pe care o pictați în continuare preia gradientul în locul unei culori plane.

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

Axa aici este verticală, de la y=100 la y=260 la un x fix, astfel încât benzile de culoare rulează orizontal, iar dreptunghiul trece treptat de la albastru la baza sa la alb la vârf. Deoarece shader-ul este identificat prin nume, o singură definiție poate umple orice număr de forme de pe pagină, iar revenirea la o culoare plană este doar un alt apel SetFillColor înainte de următoarea cale.

Modelele de placare repetă o celulă

Acolo unde un gradient variază o singură culoare în mod fluid, un model de placare (tiling pattern) repetă o mică bucată de lucrare artistică pe o regiune. ISO 32000-1 §8.7.3.1 definește un model de placare ca o celulă de model, o bucată independentă de conținut pe care redatorul o replică pe o grilă fixă pentru a placa zona pictată. Acesta este modul în care construiți hașurarea pentru o umplere inginerească, un motiv de brand care se repetă în spatele unui antet sau un fundal texturat care rămâne clar ca vector și nu cântărește aproape nimic, indiferent cât de mare este zona, deoarece celula este stocată o singură dată și menționată peste tot.

PDFlibPas construiește celula de model din conținutul paginii capturat. Capturați o pagină sau o regiune cu CapturePage, transformați captura într-un model numit cu NewTilingPatternFromCapturedPage(PatternName, CaptureID) și apoi selectați acel model ca umplere curentă cu SetFillTilingPattern(PatternName). Din acel moment, orice cale pe care o umpleți este pictată cu celula repetitivă în locul unei culori plane, exact așa cum funcționează o umplere cu shader, dar cu o celulă placată ca sursă de desen. Secvența este mai complexă decât un singur apel, așa că, dacă pasul de captură este necunoscut, tratați modelul ca pe o operațiune în două etape: produceți mai întâi celula capturată, apoi asociați-o ca umplere după nume înainte de a desena regiunea pe care doriți să o placați.

Punerea primitivelor împreună

Piesele se compun direct. O formă Bezier umplută este o cale de curbe desenată cu DrawPath. Același contur desenat cu DrawPathEvenOdd după adăugarea unei bucle interioare arată un gol pe care umplerea prin înfășurare l-ar fi închis. Un dreptunghi umplut cu gradient este o casetă asociată unui shader. Exemplul de mai jos desenează toate cele trei în secvență, astfel încât diferența dintre cele două reguli de umplere să fie vizibilă pe o singură pagină, apoi așază un panou cu gradient sub ele.

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

Merită reținute două detalii. Apelul de desenare decide regula de umplere, astfel încât alegerea între DrawPath și DrawPathEvenOdd este alegerea între înfășurarea diferită de zero și par-impar, iar pentru formele cu goluri regula par-impar vă scutește de analizarea direcției sub-căii. Iar starea grafică este eșantionată în momentul în care desenați: setați culorile, lățimea liniei și asocierea shader-ului înainte de apelul de desenare, deoarece aceasta este starea pe care o citește motorul. Construiți mai întâi, configurați starea, desenați la sfârșit, iar modelul vectorial se va comporta previzibil de fiecare dată.

De aici, următorii pași firești sunt citirea vectorilor și a textului înapoi dintr-un document existent, acoperită în articolul nostru despre extragerea de text, imagini și fonturi, și redarea aceluiași model de desen pe un context de dispozitiv Windows pentru previzualizare pe ecran și tipărire, acoperită în ghidul de tipărire și previzualizare. Apelurile de cale, shader și model descrise aici sunt livrate ca parte a Delphi PDF Library, alături de API-urile de text, imagini, formulare și semnături acoperite în alte părți pe acest blog.