O pagină PDF nu stochează pixeli și nici nu stochează un arbore de obiecte-formă așa cum face SVG. Ea stochează un program. Fiecare linie, curbă, umplere și imagine plasată în pagină este rezultatul execuției unei secvențe de operatori dintr-un flux de conținut, de sus în jos, peste o stare grafică activă. Înțelegeți acest singur fapt și cea mai mare parte a comportamentului formatului încetează să surprindă: de ce o umplere are nevoie de un operator de pictare separat după ce traseul a fost construit, de ce culorile și grosimile de linie se scurg dintr-o formă în următoarea dacă nu le încadrați, de ce același cod de desenare poate ajunge în locuri complet diferite după o singură transformare de coordonate. Acesta este un tur al modelului de execuție definit în ISO 32000: operatorii pe care îi întâlniți când deschideți un flux de conținut și regulile care decid ce apare pe pagină
Fluxul de conținut este bytecode postfixat
Un flux de conținut este o secvență plată de octeți formată din operanzi urmați de operatori. Operanzii vin primii, operatorul care îi consumă vine ultimul, ceea ce este inversul unui apel de funcție și identic cu o mașină cu stivă: puneți numerele pe stivă, apoi emiteți verbul. Nu există imbricare, nu există sintaxă de expresii, nu există variabile. Conturul unui triunghi înseamnă cinci rânduri de acest fel:
100 100 m % moveto: începe un subtraseu nou la (100, 100)
200 200 l % lineto: adaugă un segment până la (200, 200)
300 100 l % lineto: adaugă un segment până la (300, 100)
h % closepath: leagă înapoi la punctul de start
S % stroke: trasează conturul traseului
Operatorii sunt laconici intenționat. O pagină reală înseamnă mii de asemenea instrucțiuni, de obicei comprimate cu FlateDecode. Prețul acestei compactități este că fluxul nu poartă nicio structură pe care să o puteți interoga: un vizualizator nu poate întreba „unde este titlul din această pagină”, poate doar să ruleze programul și să vadă unde ajunge cerneala. Acesta este motivul de fond pentru care extragerea textului din PDF-uri arbitrare este dificilă
Originea este jos-stânga, iar Y crește în sus
Înainte ca o coordonată să aibă vreun sens, trebuie să știți unde se află (0, 0). PDF pune originea în colțul din stânga-jos al paginii, cu X crescând spre dreapta și Y crescând în sus, măsurat în puncte, la 72 de puncte pe țol (ISO 32000-2 §8.3.2). Pe o pagină US Letter, marginea de sus se află la y = 792, nu la y = 0. Oricine vine dinspre grafica de ecran, unde originea este sus-stânga și Y crește în jos, o ia invers din prima încercare și desenează prima linie dincolo de marginea de jos a paginii. Unitatea este și ea independentă de mediu: 72 de unități înseamnă un țol fie că pagina se randează pe ecranul unui telefon, fie pe un fotocompozitor
Majoritatea bibliotecilor de desenare în pagină moștenesc direct această convenție. În HotPDF, de pildă, TextOut și apelurile de traseu măsoară toate din colțul stânga-jos, în puncte, așa că o valoare apropiată de înălțimea paginii duce conținutul sus:
// HotPDF, Delphi: y măsurat de la marginea de jos în sus, în puncte
Pdf.CurrentPage.SetLineWidth(2.0);
Pdf.CurrentPage.MoveTo(100, 700); // aproape de partea de sus a paginii
Pdf.CurrentPage.LineTo(300, 700);
Pdf.CurrentPage.Stroke; // emite operatorii moveto/lineto/stroke
Acea secvență de apeluri se compilează exact în operatorii m, l și S de mai sus. Biblioteca este un dactilograf pentru fluxul de conținut, nimic mai mult, iar faptul că știți ce emite este ceea ce vă permite să raționați despre rezultat atunci când o formă ajunge undeva unde nu vă așteptați
Construiți traseul, apoi pictați-l
PDF separă construcția traseului de pictarea lui, iar separarea nu este pedanterie. Descrieți întâi o formă cu operatori de construcție care nu adaugă nimic vizibil, apoi emiteți un singur operator de pictare care decide ce se face cu traseul acumulat. Același triunghi poate fi un contur, o umplere plină sau ambele, în funcție doar de verbul cu care încheiați
Operatorii de construcție sunt puțini. m începe un subtraseu nou într-un punct. l adaugă un segment drept. c adaugă o curbă Bezier cubică din șase operanzi, două puncte de control și un punct final. re este o scurtătură care adaugă un dreptunghi întreg dintr-un cvartet x, y, lățime, înălțime. h închide subtraseul curent înapoi la punctul lui de start. Niciunul dintre ei nu pune cerneală pe pagină; ei doar acumulează geometrie
200 250 m % începe subtraseul
300 350 400 450 500 250 c % Bezier cubică: două puncte de control, apoi punctul final
150 200 re % un dreptunghi de 150 x 200, adăugat ca subtraseu propriu
h % închide
Exemplul original folosea varianta y, acum ieșită din uz, a operatorului de curbă; c, cu cele trei puncte explicite ale sale, este forma pe care o veți vedea în practică și cea la care merită să apelați. Odată ce traseul există, un singur operator de pictare îl încheie. Vocabularul este mic și merită memorat, pentru că fiecare formă de pe fiecare pagină se termină cu unul dintre aceștia:
Strasează conturul traseului folosind grosimea de linie și culoarea de contur curentefumple interiorul folosind culoarea de umplere curentă și regula de înfășurare nenulăf*umple folosind regula par-impar, care contează pentru formele autointersectate și pentru cele cu goluriBumple și apoi trasează conturul într-o singură operație;bînchide mai întâi traseulnnu pictează nimic, iar așa devine un traseu regiune de decupare fără să lase o urmă vizibilă
Regula de înfășurare este partea pe care lumea o greșește. Cea nenulă (f, B) numără traversările cu semn ale unei raze pornite din punctul de test și umple oriunde numărătoarea nu este zero, așa că un gol rămâne gol doar dacă subtraseul lui se înfășoară invers față de cel exterior. Par-impar (f*, B*) comută la fiecare traversare, indiferent de direcție. Dacă o formă de tip „gogoașă” iese plină, cercul interior este înfășurat în același sens ca cel exterior, iar dumneavoastră fie îl inversați, fie treceți la par-impar
Culoarea este un mod, nu un parametru
Culoarea dintr-un flux de conținut este lipicioasă. Setați o culoare și ea rămâne setată până când setați alta sau restaurați o stare anterioară, motiv pentru care o schimbare de culoare neîncadrată colorează pe tăcute tot ce se desenează după ea. PDF ține și culoarea de umplere și pe cea de contur ca două setări independente, cu operatori scriși cu minuscule pentru umplere și cu majuscule pentru contur. Spațiile de culoare de dispozitiv au fiecare propria prescurtare:
0.5 g % umplere DeviceGray, gri mediu (0 = negru, 1 = alb)
0.2 0.6 0.8 rg % umplere DeviceRGB
0.8 0.2 0.1 RG % contur DeviceRGB (majuscule = contur)
0.2 0.8 0.0 0.1 k % umplere DeviceCMYK
DeviceRGB se potrivește ieșirii pe ecran, DeviceCMYK este ceea ce așteaptă producția tipografică, iar DeviceGray este alegerea cea mai economică pentru conținut monocrom. Spațiile de dispozitiv sunt comode, dar necalibrate: același triplet RGB se poate randa diferit pe două monitoare, iar aceasta este problema pe care există să o rezolve spațiile de culoare bazate pe ICC și intențiile de ieșire PDF/A. Pentru lucrările în care culoarea este critică, selectați un spațiu calibrat cu cs și CS și setați componentele cu sc și scn, dar pentru documentele obișnuite prescurtările de dispozitiv duc greul. O bibliotecă le împachetează în apeluri tipizate. HotPDF, de exemplu, primește un singur TColor și emite operatorii corespunzători:
Pdf.CurrentPage.SetRGBFillColor(clRed);
Pdf.CurrentPage.Rectangle(100, 100, 200, 150); // x, y, lățime, înălțime
Pdf.CurrentPage.Fill;
Pdf.CurrentPage.SetRGBFillColor(RGB(0, 255, 0));
Pdf.CurrentPage.Circle(150, 400, 50); // x, y, rază
Pdf.CurrentPage.Fill;
Starea grafică și stiva q/Q
Tot ce nu este traseul însuși trăiește în starea grafică: matricea de transformare curentă, culorile de umplere și de contur, grosimea liniei, modelul de linie întreruptă, regiunea de decupare, transparența. Starea este globală și mutabilă, așa că singura cale sigură de a face o modificare locală este să salvați totul, să modificați, să desenați și apoi să reveniți. Exact asta fac q și Q. q pune o copie a stării curente pe o stivă; Q o scoate, aruncând orice modificare făcută de la perechea q încoace
q % salvează întreaga stare grafică
2 0 0 2 100 100 cm % concatenează o transformare: scalare 2x, translatare la (100,100)
0.8 g % umplere gri, limitată la acest bloc
% ... desenează conținut scalat, gri ...
Q % restaurare: transformarea și culoarea revin
Perechile dezechilibrate q și Q sunt un mod obișnuit în care un flux de conținut construit manual sau lipit din bucăți o ia razna. Un q rătăcit, fără Q pereche, lasă stiva adâncă la finalul paginii; un Q în plus o duce sub zero. În oricare dintre cazuri, un vizualizator poate ține în vigoare o decupare sau o transformare veche, iar conținutul dispare sau ajunge într-un loc greșit. Când grafica se volatilizează fără un motiv pe care traseul să îl explice, auditați mai întâi stiva de stare
CTM transformă fiecare coordonată
Matricea de transformare curentă stă între numerele din operatorii dumneavoastră și pagina propriu-zisă. Fiecare coordonată este înmulțită cu CTM înainte ca ceva să fie desenat, așa că schimbarea matricei schimbă unde și cum apare tot ce se desenează în continuare, fără să atingă nicio coordonată de traseu. Operatorul cm concatenează o matrice nouă peste cea curentă, primind șase operanzi care corespund matricei afine [a b c d e f]:
1 0 0 1 100 50 cm % translatare cu (100, 50): e și f poartă deplasarea
2 0 0 1.5 0 0 cm % scalare x cu 2, y cu 1.5: a și d sunt factorii de scalare
0.707 0.707 -0.707 0.707 0 0 cm % rotire cu 45 de grade (cos/sin în a, b, c, d)
Două lucruri încurcă lumea. Întâi, cm compune în loc să înlocuiască, așa că transformările se acumulează și ordinea contează: scalarea urmată de translatare nu este același lucru cu translatarea urmată de scalare. Apoi, rotirea și scalarea pivotează în jurul originii curente, nu în jurul centrului formei dumneavoastră, așa că, pentru a roti ceva pe loc, îl translatați la origine, rotiți, apoi translatați înapoi, totul încadrat în q/Q. Aceeași matrice este cea care plasează imaginile, ultima piesă care merită văzută
Imaginile și conținutul reutilizabil sunt XObject-uri
Imaginile raster nu stau inline în fluxul de conținut. Ele sunt stocate ca imagini XObject, obiecte externe cu propriul dicționar care descrie lățimea, înălțimea, adâncimea de bit, spațiul de culoare și filtrul de compresie, iar fluxul de conținut doar face referire la ele. O fotografie susținută de un JPEG se declară așa:
/Photo <<
/Type /XObject
/Subtype /Image
/Width 640
/Height 480
/BitsPerComponent 8
/ColorSpace /DeviceRGB
/Filter /DCTDecode % datele imaginii sunt un flux JPEG
>>
O imagine XObject se desenează în pătratul unitate: ocupă întotdeauna regiunea de la (0, 0) la (1, 1) în spațiul utilizator. Nu îi transmiteți o poziție sau o dimensiune. În schimb, setați CTM astfel încât acel pătrat unitate să fie mapat pe dreptunghiul dorit, apoi o invocați cu Do. De aceea plasarea unei imagini înseamnă întotdeauna o transformare urmată de o invocare, încadrate într-o pereche salvare/restaurare, ca scalarea să nu se scurgă în operația următoare:
q
640 0 0 480 50 300 cm % mapează pătratul unitate pe o casetă 640x480 la (50, 300)
/Photo Do % pictează imaginea XObject
Q
Același mecanism Do conduce și XObject-urile de tip formular, care păstrează o bucată reutilizabilă de grafică, un logo sau o ștampilă repetată, ca flux de conținut propriu cu o casetă de încadrare. O definiți o dată, o invocați de multe ori cu un CTM diferit, iar octeții apar în fișier o singură dată. Majoritatea bibliotecilor ascund asta în spatele unui singur apel de plasare: HotPDF înregistrează o imagine bitmap cu AddImage și o plasează cu ShowImage, primind explicit x, y, lățime și înălțime în loc să vă ceară să construiți matricea de mână:
var
Bmp: TBitmap;
ImgIndex: Integer;
begin
Bmp := TBitmap.Create;
try
Bmp.LoadFromFile('logo.bmp');
ImgIndex := Pdf.AddImage(Bmp, icFlate);
// x, y (stânga-jos), lățime, înălțime, unghi de rotire
Pdf.CurrentPage.ShowImage(ImgIndex, 50, 300, 200, 150, 0);
finally
Bmp.Free;
end;
end;
Sub acel unic rând, biblioteca scrie dicționarul imaginii XObject, setează CTM pentru a dimensiona și poziționa pătratul unitate și emite Do. Modelul de dedesubt este cel care merită cunoscut, pentru că el explică orice rezultat ciudat: o imagine întinsă este un CTM cu factori de scalare nepotriviți, un logo identic pe patruzeci de pagini este un singur XObject de tip formular invocat de patruzeci de ori, iar o imagine care se randează cu susul în jos este o inversare de semn în matrice, nu un fișier corupt
Unde duce toate acestea
Modelul grafic este mic odată ce îi vedeți forma. Un flux de conținut este bytecode postfixat care rulează peste o stare mutabilă; coordonatele pornesc din colțul stânga-jos și trec prin CTM; traseele se construiesc în tăcere și se pictează cu un singur operator deliberat; setările de culoare și de linie persistă până când le încadrați cu q/Q; imaginile și grafica reutilizabilă sunt XObject-uri plasate prin transformarea unui pătrat unitate. Aproape orice rezultat de randare derutant se reduce la una dintre aceste cinci reguli. Dacă vreți să vedeți cum se așază acești operatori grafici în modelul de obiecte mai larg, în dicționarele de pagină și în tabelul de referințe încrucișate care le indică, privirea de ansamblu tehnică asupra structurii unui fișier PDF acoperă acel strat, iar construirea unui PDF simplu de la zero parcurge octeții de la un capăt la altul. Desenarea textului trăiește în propria familie de operatori și are propriile capcane, tratate în articolul însoțitor despre textul și gestionarea fonturilor în PDF
Apelurile de desenare Delphi arătate aici, MoveTo, LineTo, Stroke, Rectangle, Fill, SetRGBFillColor, AddImage și ShowImage, fac parte din HotPDF Delphi Component pentru Delphi și C++Builder, care emite acești operatori de flux de conținut în locul dumneavoastră