HotPDF randează fonturile PDF încorporate în Delphi fără a instala nimic pe mașină: conducta de randare a glyph-urilor încorporate din HPDFGlyphRender.pas analizează programele de font stocate în interiorul PDF-ului însuși — contururile TrueType glyf din FontFile2, charstrings CFF Type 2 din FontFile3 și fluxurile de conținut ale glyph-urilor Type 3 — și le redă ca căi vectoriale GDI umplute. Acest articol este o analiză aprofundată a fidelității fonturilor din spatele randării paginilor PDF într-un TBitmap cu HotPDF: acel articol acoperă randarea ca întreg, iar acesta explică modul în care textul de pe acele pagini își obține formele exacte
De ce se randează un PDF cu casete (tofu) în loc de text?
Casetele, spațiile goale sau caracterele ușor greșite în rezultatul randat al unui PDF înseamnă aproape întotdeauna că motorul de randare a solicitat sistemului de operare un font în loc să îl folosească pe cel încorporat în fișier. Reclamația ajunge în același mod de fiecare dată: documentul arată perfect pe mașina pe care a fost produs, apoi un client îl deschide pe un server curat sau pe un desktop cu permisiuni limitate și factura în limba japoneză afișează pătrate de tip tofu, sau un font înlocuitor similar decalează fiecare sfârșit de linie. Fonturile nu au fost niciodată pe acea mașină — ci doar în interiorul PDF-ului — iar un motor de randare care se limitează la înlocuirea cu fonturi de sistem nu le poate afișa. Fonturile de tip subset înrăutățesc lucrurile: un subset poate transporta patruzeci de glyph-uri sub coduri de caractere atribuite în mod privat acelui singur fișier, o atribuire pe care niciun font instalat nu o partajează
ISO 32000-1 §9.9 definește trei containere pentru un program de font încorporat în descriptorul de font: FontFile conține un program original Type 1, FontFile2 un program TrueType, iar FontFile3 un CFF simplu (Type1C sau CIDFontType0C) sau un pachet OpenType. O a patra variantă, fontul Type 3 din ISO 32000-1 §9.6.5, nu încorporează deloc date binare — fiecare glyph este un mic flux de conținut PDF executat în loc. Cele trei containere diferă ca matematică a conturului (B-spline-uri pătratice versus charstrings cubice versus operatori de pagină arbitrari), astfel încât un motor de randare fidel are nevoie de un interpret separat pentru fiecare, plus un strat de codificare care transformă codurile de caractere în indexul de glyph corect înainte ca orice contur să fie procesat
Cum convertește HotPDF contururile TrueType glyf în căi GDI?
THPDFEmbeddedTTF din HPDFGlyphRender.pas citește tabelul loca pentru a localiza fiecare înregistrare de glyph, parcurge contururile glyf punct cu punct și emite o cale GDI. Două convenții TrueType necesită gestionare explicită. În primul rând, punctele consecutive din afara curbei (off-curve) implică un punct pe curbă (on-curve) la mijlocul lor, iar un contur ale cărui puncte sunt toate în afara curbei începe la mijlocul ultimului și primului său punct — dacă omiteți oricare dintre aceste reguli, glyph-urile rotunjite capătă fațete plate sau se prăbușesc. În al doilea rând, curbele TrueType sunt Bézier pătratice, în timp ce metoda PolyBezierTo din GDI primește cubice, astfel încât fiecare segment pătratic este elevat ca grad în mod exact, mai degrabă decât aplatizat în segmente de linie
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
Elevarea gradului este fără pierderi: cubica trasează o curbă identică, astfel încât conturul randat se potrivește cu ceea ce un vizualizator conform desenează din același tabel, la orice zoom. Activitatea rămasă este plasarea. Fiecare glyph este creat în unități de font (de obicei o grilă de 1000 sau 2048 de unități per em), iar motorul de randare compune matricea de scalare, matricea de text și matricea curentă de transformare într-o singură transformare glyph-la-dispozitiv înainte ca calea să fie umplută. Ordinea contează aici mai mult decât pare: compuneți aceleași trei matrice în sens invers și fiecare glyph se prăbușește spre origine — o pagină care arată greșit, dar a cărei eroare reală este o singură linie de algebră matriceală
Cum gestionează interpretul de charstring Type 2 fonturile CFF
THPDFEmbeddedCFF oferă programelor FontFile3 un interpret real de charstring Type 2: analizează structurile CFF INDEX, Top DICT și Private DICT, apoi execută fiecare charstring și emite segmente de cale direct către GDI. Un pachet OpenType (containerul OTTO) este mai întâi curățat pentru a ajunge la tabelul CFF gol; fluxurile simple CIDFontType0C și Type1C sunt consumate direct. Charstrings sunt un limbaj compact bazat pe stivă, iar trei dintre convențiile sale decid dacă interpretul rămâne sincronizat cu fluxul de octeți. Prefixul opțional de lățime înseamnă că primul operator care golește stiva poate conține un operand suplimentar. Operatorul hintmask implică un vstemhm când operanzii se află încă pe stivă, iar numărul de octeți de mască de omis depinde de numărul de tije (stems) acumulate — greșiți numărul o singură dată și fiecare cod operațional următor va fi citit greșit. Iar apelurile de subrutine adaugă o deviație (bias) la indexul lor (107, 1131 sau 32768, în funcție de numărul de subrutine) înainte de căutare, astfel încât un apel fără această deviație ajunge la o subrutină complet greșită
Formatul CFF cu cheie CID adaugă o indirecție care pune probleme implementărilor simple: codul de caracter selectează un CID, dar indexul charstring este un GID, iar setul de caractere al fontului (charset) asociază GID cu CID — astfel încât motorul de randare construiește maparea inversă CID-la-GID înainte de a desena și selectează Private DICT-ul specific fiecărui glyph prin FDSelect pentru fonturile care conțin mai multe. Programele Type1C cu cheie de nume, containerul obișnuit pentru fonturile simple Type 1, rezolvă în schimb codurile de un octet prin codificarea încorporată a programului CFF sau prin mecanismul de codificare la nivel de PDF descris în continuare. O avertizare onestă: interpretul citește operatorii de hinting pentru a menține fluxul sincronizat, pero nu execută hinting-ul în sine, o limitare discutată la sfârșit
Ce este un font Type 3 și cum se desenează acesta?
Un glyph Type 3 nu este deloc un contur — ISO 32000-1 §9.6.5 îl definește ca un flux de conținut, prin urmare HotPDF îl randează prin salvarea stării grafice pe stivă (pushing the graphics state), compunerea matricei de font, dimensiunii fontului și matricei de text pe CTM, și executarea procedurii glyph prin același interpret de operatori care desenează paginile, având propriul /Resources al fontului în domeniu. Două detalii din specificație contează pentru corectitudine. Lățimile /Widths Type 3 sunt exprimate în spațiul glyph-ului în loc de spațiul de text 1/1000 pe care îl folosește orice alt tip de font, deci deplasările trebuie să treacă prin /FontMatrix — altfel fonturile de coduri de bare cu o matrice de 0.01 fac pași greșiți cu un ordin de mărime. Iar o procedură glyph care se deschide cu operatorul d1 face două promisiuni pe care motorul de randare le impune: desenarea este decupată conform casetei de încadrare (bounding box) declarate, iar conform ISO 32000-1 §9.6.5.2 glyph-ul ignoră propriii operatori de culoare și desenează cu culoarea de umplere curentă a apelantului, deci rg, g, k și versiunile lor de contur din interiorul procedurii sunt dezactivate pe durata acelui glyph. Dacă omiteți regula culorii, un font de cod de bare d1 aplicat cu albastru de pagină va ieși negru; dacă omiteți decuparea, un glyph deformat va desena în afara celulei sale
Cum devin codurile de caractere ID-uri de glyph
Interpreții de contur sunt doar jumătate din activitate, deoarece octeții dintr-un șir de text PDF sunt coduri de caractere, nu indici de glyph, iar ISO 32000-1 dedică două subclauze acestei mapări. Pentru fonturile simple, §9.6.6 prescrie o prioritate strictă: un tablou /Differences suprascrie codificarea de bază (WinAnsiEncoding, MacRomanEncoding sau StandardEncoding), care suprascrie propria hartă a programului de font. HotPDF rezolvă acel lanț într-un tabel cod-la-GID de 256 de intrări, traducând numele de glyph-uri în indici de glyph-uri prin trei căi: potrivirea exactă a setului de caractere în interiorul unui program CFF, numele numerice gNN/glyphNN luate ca indici literali și traducerea din Adobe Glyph List nume-la-Unicode urmată de o căutare cmap pentru programele TrueType. Pentru fonturile compozite, §9.7 pune în funcțiune CIDToGIDMap: cazul comun este /Identity, dar intrarea poate fi un flux de perechi big-endian indexate după CID — iar propria ieșire Unicode a HotPDF folosește exact această formă de flux pentru subseturi compacte, deci calea de flux nu este o situație exotică
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
Când este necesară o căutare în cmap TrueType, HotPDF parcurge un lanț de rezervă în loc să aibă încredere într-un singur subtabel: subtabelele Unicode Windows (formatul 4, apoi formatul 12 pentru planurile suplimentare) sunt pe primul loc, apoi subtabelul de simboluri (3,0) cu convenția sa de zonă de utilizare privată F000 oglindită în octetul inferior — motivul pentru care un font de simboluri precum Wingdings răspunde pentru codurile ASCII simple — apoi formatele moștenite 6 și 0. Subtabelele format 2 nu sunt interpretate în mod deliberat: ele mapează pagini de coduri multi-octet vechi, cum ar fi Shift-JIS și Big5, nu Unicode, iar fonturile CJK moderne conțin oricum un subtabel format 4 sau format 12. Orice cod care nu supraviețuiește niciuneia dintre aceste căi recurge la desenarea GDI pentru acel singur glyph, astfel încât un caracter imposibil de mapat afectează doar un singur glyph, nu întreaga secvență de text
Ce nu realizează calea de randare încorporată
Limitele merită expuse clar. Hinting-ul nu este executat — contururile sunt umplute exact așa cum au fost create, ceea ce nu se poate distinge de rezultatul cu hinting la 150 DPI și peste, pero poate diferi cu un pixel de un rasterizator cu hinting la dimensiuni foarte mici. Programele originale Type 1 din FontFile (charstrings criptate eexec) nu sunt interpretate, iar axele fonturilor variabile OpenType nu sunt aplicate; ambele cazuri, la fel ca un program de font deteriorat sau un tabel glyf fără un cmap utilizabil, recurg la desenarea cu fonturi de sistem în loc să ducă la eșecul paginii. Aceeași abordare axată pe fidelitate se extinde și în alte părți ale motorului de randare — modelele de umbrire axială și radială primesc același tratament pe care îl merită gradienții — iar partea de generare are propria poveste legată de subtilitatea fonturilor în modul în care EndDoc ordonează subseturile de fonturi
Utilizarea conductei nu necesită deloc cod specific pentru fonturi — fiecare mecanism de mai sus este activat automat în cadrul apelului de randare a paginii
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
Consecința practică este cea de care se preocupă echipa dvs. de asistență: un PDF care își poartă fonturile se randează cu acele fonturi pe un server de build, într-un container Windows sau pe desktopul unui client care nu a văzut niciodată acel font. Conducta de randare a glyph-urilor încorporate este livrată ca parte a HotPDF Component pentru Delphi și C++Builder — o bibliotecă VCL nativă care acoperă crearea, editarea, extragerea de text și randarea paginilor PDF fără dependențe de DLL-uri externe