momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Caractere (vechi şi noi), cu PostScript (III)

PostScript
2020 may

Caractere şi metrici

Mai încolo vom schiţa o procedură PS prin care putem reda graficele unor caractere (fie „pline”, prin fill, fie conturate, prin stroke) din diverse fonturi (la o scară convenabil de mare), evidenţiind şi metricile principale asociate lor:


Fig.1

Pentru un caracter oarecare, pe lângă graficul acestuia, sunt trasate trei linii: „linia de bază(de pe orizontala imaginară pe care se bazează scrierea unor caractere pe un acelaşi rând al paginii) şi verticalele boxei care ar încadra graficul caracterului; mai jos referim aceste linii şi prin notaţiile Lb, Vl ("left") şi respectiv Vr ("right").

Capătul din stânga al liniei de bază este originea O(0, 0), a sistemului de coordonate al caracterului; capătul final O(w, 0) al lui Lb este originea Ou(0, 0) a caracterului care ar putea urma pe acelaşi rând celui de lăţime w curent. Fixând astfel O şi Ou pentru fiecare caracter, se asigură în mod implicit o anumită distanţare între caracterele scrise consecutiv (prin operatorul show) pe un acelaşi rând (dar în Fig.1, cele patru caractere au fost distanţate convenabil în mod explicit, prin translate). Pentru PS, lăţimea caracterului este distanţa dintre O şi Ou (şi nu cea dintre Vl şi Vr).

Subliniem că—dependent de font şi de caracter—O se poate afla fie în stânga, fie în dreapta lui Vl, iar Ou se poate afla fie în dreapta, fie în stânga lui Vr. De exemplu, (v. Fig.1) în fontul Times-Italic, graficul lui "A" începe ceva mai la stânga originii (intrând puţin în zona graficului precedent pe linia curentă); în fontul URWGothic-BookOblique, graficul lui "g" se încheie puţin mai la dreapta originii graficului următor.
Iar Lb poate să taie sau să atingă graficul (şi implicit, Vl şi Vr), dar poate fi şi complet în afara acestuia (de exemplu în cazul unor accente, sau în cazul unor „cratime”).

Pentru un caracter oarecare, consemnăm codul şi numele acestuia, fontul din care provine, abscisele "x:" pentru Vl, Vr şi Ou (ordonate crescător), precum şi ordonatele "y:" ale capetelor verticalelor Vl şi Vr; precizăm că am „multiplicat” cu 1000 coordonatele respective, evitând astfel apariţia a câte două caractere suplimentare (scriind de exemplu "y: -36:591", în loc de "y: -0.36:0.591" cum ar fi fost „corect”).

Astfel, primul caracter reprezentat în Fig.1 are codul 103, numele "g" şi provine din fontul "Times-Roman"; notaţia "x: 28:470:500" ne spune că verticalele de încadrare au abscisele 28 şi 470, iar Ou are abscisa 500; notaţia "y: -218:460" ne indică ordonatele între care este cuprins graficul. Deducem imediat că boxa de încadrare a graficului acestui caracter are colţul din stânga-jos (28, -218) şi colţul din dreapta-sus (470, 460); în stânga şi în dreapta graficului caracterului sunt lăsate 28 şi respectiv, 500 - 470 = 30 de „puncte” (deci distanţa faţă de precedentul şi respectiv, următorul grafic de pe linia curentă este de cel puţin 28 şi respectiv, 30 de puncte tipografice).

Desigur, împărţind la 1000 şi înmulţind cu „mărimea fontului”, găsim şi coordonatele sau distanţele „reale”; de exemplu, dacă fontul respectiv a fost scalat la 24 de puncte, atunci lăţimea boxei de încadrare a caracterului respectiv este (470 - 28)*0.024 = 10.608 puncte tipografice = 10.608/72 = 0.1473inch = 3.74mm; iar lăţimea caracterului este abscisa lui Ou, deci este 500*0.024/72 = 0.166inch = 4.216mm (dar valorile „reale” depind puţin, până la urmă, şi de rezoluţia ecranului sau imprimantei care va produce în final caracterul).

Fişierul metric asociat fontului

În /usr/share/ghostscript/9.26/Resource/Font găsim cele 35 de fişiere-font PS–2 „de bază” (v. PostScript_fonts), în versiunea gratuită produsă de firma URW++; în catalogul "Fontmap.GS" din Resource/Init, găsim asocierile acestora cu fonturile PS originale (de la Adobe) – de exemplu, fişierul Font/NimbusRoman-Regular corespunde fontului Times-Roman.

Fişierele-font găsite corespund formatului PS-Adobe-1 ("Type 1") şi nu explicitează informaţii metrice pentru caracterele modelate (la o adică, acestea s-ar putea deduce dezasamblând secvenţele de cod care produc graficele caracterelor, încorporate într-o anumită criptare, în fişierele-font respective). Dar putem obţine fişierele metrice asociate "*.afm", fie direct de la ArtifexSoftware, fie instalând pachetul fonts-urw-base35 de la Ubuntu.

Într-un fişier .afm ("Adobe Font Metric") găsim o secţiune de linii pe care se precizează pentru fiecare caracter din fontul respectiv, codul, numele, lăţimea caracterului şi două colţuri opuse ale boxei de încadrare a graficului; de exemplu, linia

C 103 ; WX 500 ; N g ; B 28 -218 470 460 ;   % NimbusRoman-Regular (v. şi Fig.1)

corespunde caracterului cu numele "g", de cod 103, având lăţimea 500 şi al cărui grafic este încadrat în boxa cu colţul stânga-jos (28, -218) şi colţul dreapta-sus (470, 460). Coordonatele şi dimensiunile sunt referite faţă de scara de 1000 puncte tipografice.

Pentru caracterul ".notdef" şi pentru caracterele necodificate (adică neînregistrate în tabloul iniţial "Encoding" din dicţionarul fontului), linia începe cu "C -1"; de exemplu:

C 251 ; WX 500 ; N germandbls ; B 12 -9 468 683 ;  % ultimul din tabloul "Encoding"
C -1 ; WX 250 ; N .notdef ; B 125 0 125 0 ;  % grafic vid (doar mută puţin originea)
C -1 ; WX 444 ; N abreve ; B 37 -10 442 664 ;  % pentru 'ă', în NimbusRoman-Regular

Este uşor de interpretat aceste informaţii în spiritul reprezentărilor şi notaţiilor introduse mai sus; de exemplu, pentru linia corespunzătoare caracterului "slash" în fontul Times-Italic:

C 47 ; WX 278 ; N slash ; B -65 -18 386 666 ;  % NimbusRoman-Italic.afm

avem: Ou(278, 0), abscisa lui Vl este negativă (= -65), iar abscisa lui Vr (= 386) este mai mare ca a lui Ou – încât deducem că slash „intră” câte puţin (chiar „binişor”: 65 şi respectiv, 386-278=108 puncte tipografice) în zonele caracterelor între care este scris.

Mai departe vom schiţa modul în care putem folosi aceste linii din fişierele metrice .afm, pentru a reprezenta caracterele precum în Fig.1.
Nu avem totuşi, un răspuns solid (afară de cel mai simplu, „la nimic”) pentru întrebarea firească: la ce ar folosi o astfel de reprezentare ? Este de presupus că programarea necesară este instructivă, implicând lămurirea anumitor aspecte legate de PS şi de fonturi; pe de altă parte, poate să ne amintim de începuturile tipografiei digitale, când asemenea reprezentări (la scară mare) erau folosite frecvent pentru a proba caracterul tocmai creat ("proof", sau "Hardcopy Proofs" în programul şi cartea de METAFONT ale lui Knuth), a-l compara cu celelalte şi a decide ce modificări ar mai fi încă de făcut asupra graficului pentru a-l integra sub toate aspectele fontului respectiv.

Procedură PS pentru „probarea” unui caracter

Dezvoltăm „pas cu pas” programul PS prchar.ps (precizăm că unele aspecte implicate—de exemplu, „Schimbarea fontului”—au fost explicate deja, în părţile anterioare – v. [1]).

%!
/FS {findfont exch scalefont} bind def  % <24> </Courier> FS  (v. „Partea I”)
% setăm fonturi adecvate scrierii de numere şi respectiv, de nume
/sz 8 def  % mărimea de caracter pentru scrierea metricilor din AFM
/_num {//sz /Courier FS setfont} bind def  % pentru scrierea numerelor
/_str {//sz /Helvetica-Narrow FS setfont} bind def  % scrierea numelor
/newl {  % "newline" (mută punctul curent la începutul liniei următoare)
    currentpoint exch pop   % x y | y x | y
    //sz sub 0 exch moveto  % y-sz 0 | 0 y-8 | moveto
} bind def
% scrie număr de max. 4 cifre (<număr> s_num)
/s_num {4 string cvs show} bind def
/octch 1 string def  % pentru conversia codului caracterului în "şir octal"

Am ales să scriem numerele (codul caracterului şi diversele coordonate) folosind fontul Courier, iar numele (de caracter, de font) folosind Helvetica-Narrow ("sans serif", condensat), fixând ca mărime valoarea indicată (8) în definiţia de constantă "/sz"; precizăm că întâlnind ulterior "//sz", interpretorul va înlocui imediat prin 8 (scutind la execuţie căutarea obişnuită, în dicţionarul procedurilor, a cheii "sz").

Desigur, puteam defini mai general procedura "/newl"; dar aici avem de scris numai rânduri de metrici, pentru care am prevăzut mărimea de caracter 8 – încât am coborât ordonata punctului curent cu 8.

Avem de scris (cu operatorul show) numere de cel mult 4 cifre (putem avea de exemplu, Ou=1053); dar „număr” înseamnă ca de obicei „număr în baza 2” – încât în "/s_num" am folosit operatorul PS cvs, pentru a obţine secvenţa corespunzătoare de cifre zecimale (furnizată apoi ca obiect "string", lui show).

Codul de caracter este şi el un număr (0..255) şi îl vom scrie ca atare, folosind s_num. Dar, pe de altă parte, vom avea de folosit charpath (v. eseu-PS.pdf) pentru a produce graficul caracterului; ori charpath cere ca argument un "string", dar nu mai este vorba acum de şir de cifre zecimale, ci de un şir care conţine un singur element – anume, reprezentarea octală a codului caracterului (prefixată cu backslash). De exemplu, pentru caracterul de cod 103 – „şirul octal” este (\147) (într-adevăr, 7+8*4+82*1 = 103) şi obţinem graficul prin (\147) true charpath.
Din acest motiv am introdus mai sus definiţia /octch, folosind-o mai încolo astfel:

    //octch 0 C put  % înscrie codul C ca "şir octal" în string-ul 'octch'
    //octch true charpath  % produce graficul şirului 

Mai departe, fixăm mărimea de redare a graficului caracterului (aici, 2 inch) şi prevedem o procedură prin care să putem obţine coordonatele „reale” din cele care iniţial se raportau la scara de 1000 de puncte tipografice:

/SZ 144 def  % mărimea de redare a graficului caracterului (2 inch)
/SZt {SZ 1000 div mul} bind def  % transformă de la scara 1000 la cea "reală"

Dacă va vrea altfel, utilizatorul va putea redefini /SZ în programul său, după ce a inclus cumva "prchar.ps"; de observat că dacă în definiţia /SZt foloseam //SZ în loc de SZ, atunci ar fi trebuit redefinit şi /SZt, nu numai /SZ.

Vom modela procedura principală după următoarea schemă de lucru:

/chSketch {
    – Argumente: - numele fontului (ex.: /Times-Roman)
                 - tablou [C WX (N) B] conform unei „linii metrice” din fişierul AFM
                 - opţional: grafic cu 'stroke' (altfel, cu 'fill')
    – /wr_metrics: subprocedură de scriere a metricilor
    – dacă C=-1 (caracter necodificat): înscrie-l  în "Encoding" (pe un loc „liber”)
    – setează fontul (după eventuala recodificare) la mărimea SZ
    – trasează (prin 'charpath') graficul caracterului (plin, sau -opţional- conturat)
    – trasează liniile Lb, Vl, Vr
    – fixează punctul curent dedesubtul graficului şi apelează 'wr-metrics'
}

De exemplu, în „partea principală” a programului am putea utiliza chSketch astfel:

36 600 translate
/Times-Italic [47 278 (slash) -65 -18 386 666] chSketch
120 0 translate
/Times-Roman [-1 444 (abreve) 37 -10 442 664] 1 chSketch

Prima invocare ar produce graficul şi metricile asociate pentru caracterul slash din Times-Italic; graficul va fi „plin”, fiindcă pe stivă s-au transmis numai două argumente. A doua invocare ar produce graficul lui abreve din Times-Roman (după ce acest caracter ar fi înscris în "Encoding") şi anume, sub forma de contur (dat fiind că acum este transmis şi un al treilea argument).

Gestionarea argumentelor procedurii

chSketch pretinde să-i fie transmise iniţial (prin stiva operatorilor) fie două, fie trei anumite valori (precizăm că ne vom scuti de a verifica tipul şi corectitudinea acestora; dacă va fi cazul, interpretorul va furniza anumite mesaje de eroare). Dacă numărul de valori găsit la execuţie în stivă—dat de count—este 3, atunci elimină din stivă al treilea argument (prin pop) şi instituie „variabila” WO=true; altfel, defineşte WO=false:

/chSketch {  % <font> <[C WX (N) B]> [<Opţional>: forţează 'stroke'] chSketch
    count 3 eq {pop /WO true def} {/WO false def} ifelse

În funcţie de valoarea găsită în WO, graficul caracterului va fi conturat cu stroke, sau va fi „umplut” prin fill.

Preluăm şi diseminăm argumentele (optând aici pentru cea mai „directă” manieră):

    /cwnb exch def  % preia tabloul de 7 elemente [C WX (N) B]
    /Font exch def  % preia numele fontului
    % Izolează componentele tabloului 'cwnb'
    /C cwnb 0 get def  % Codul caracterului, sau -1 pentru „caracter necodificat”
    /N cwnb 2 get def  % Numele caracterului
    /W cwnb 1 get def  % lăţimea caracterului ("width")
        % coordonatele celor două colţuri ale boxei de încadrare 'B'
    /Lx cwnb 3 get def
    /Ly cwnb 4 get def
    /Ux cwnb 5 get def
    /Uy cwnb 6 get def
    % Adaptează lăţimea şi coordonatele la scara 'SZ' stabilită graficului
    /Ws W SZt def
    /Lxs Lx SZt def
    /Lys Ly SZt def
    /Uxs Ux SZt def
    /Uys Uy SZt def

Transformarea prin /SZt a lăţimii şi coordonatelor ne va fi necesară pentru a trasa liniile Lb, Vl şi Vr pe graficul de mărime SZ al caracterului (iar valorile respective iniţiale—la scara 1000—vor fi necesare pentru scrierea metricilor prin wr_metrics).

Subprocedura de scriere a metricilor

Folosind _num şi s_num (şi după caz, forall) scriem codul caracterului, abscisele Lx, Ux şi W, precum şi ordonatele Ly şi Uy; în prealabil, prin subprocedura /xTick, abscisele sunt ordonate crescător (reflectând astfel ordinea vizuală a verticalelor Vl şi Vr faţă de O(0,0) şi Ou(w,0)); iar folosind _str, scriem numele caracterului, antetele "x:" şi "y:" şi respectiv, numele fontului (şi desigur, folosim newl pentru a forţa scrierea pe rândul următor):

    /wr_metrics {
        /xTick {  % subprocedură pentru a ordona crescător abscisele
            Ux W lt {[Lx (:) Ux (:) W]}
                    {[Lx (:) W (:) Ux]} 
            ifelse
        } def
        _num C s_num  % scrie codul şi numele caracterului
        _str (/) show N show  % (ex.: 170/spade)
        newl (x: ) show
        _num xTick {s_num} forall  % scrie abscisele (x: 113:629:753)
        newl _str (y: ) show
        _num [Ly (:) Uy] {s_num} forall  % scrie ordonatele (y: -36:591)
        newl _str Font 30 string cvs show  % scrie numele fontului (Symbol)
    } bind def

Bineînţeles că înainte de a instanţia wr_metrics, va trebui să fixăm „punctul curent” (de unde să înceapă scrierea), undeva dedesubtul graficului.

Să mai observăm că dacă am viza caractere dintr-un acelaşi font (formulând un „catalog” al fontului), atunci s-ar cuveni să omitem scrierea la fiecare caracter, a numelui fontului.

Cazul C=-1 (caracter necodificat); setarea fontului

Dacă numele preluat în variabila N nu este înregistrat în tabloul "Encoding" din dicţionarul fontului Font, atunci se copiază în stivă dicţionarul respectiv, se modifică pe copia respectivă tabloul "Encoding"—înscriind numele N pe o poziţie ocupată iniţial de "/.notdef", de exemplu la indexul 1—şi se salvează (sub un alt nume) fontul astfel modificat (după care, se activează acest font, la mărimea SZ, prin selectfont şi se redefineşte /C conform indexului lui N). Altfel—dacă C iniţial nu este -1—se setează Font la mărimea SZ, drept fontul curent:

    C -1 eq {  % pentru caracter necodificat iniţial (de exemplu, /abreve: 'ă')
        Font findfont dup
        length dict
        copy begin
            /Encoding Encoding 256 array copy def
            Encoding 1 N cvn put  % înscrie N la indexul 1 în "Encoding"
            /bFnt currentdict definefont pop  % salvează fontul astfel modificat
        end
        /C 1 def  % redefineşte "/C" (codul 1, în loc de -1 cât era iniţial)
        /bFnt SZ selectfont  % acum, caracterul s-ar scrie cu: (\001) show
    } {SZ Font FS setfont} ifelse

Precizăm că 'C -1 eq {...} {...} ifelse' va asigura executarea unuia sau altuia dintre cele două blocuri {...}, în funcţie de îndeplinirea sau nu, a condiţiei iniţiale 'C=-1'.

Graficul caracterului şi liniile de reper

"CharStrings" (care este un subdicţionar al dicţionarului fontului) asociază fiecărui nume de caracter câte o subrutină (care iniţial—în fişierul fontului—este criptată, ascunzând-o publicului) prin care se produce graficul caracterului, prin combinarea anumitor instrucţiuni moveto, lineto, curveto (care trasează cubice Bézier) şi closepath. În principiu, subrutina respectivă este executată numai la prima întâlnire a caracterului şi traseul rezultat este salvat în „memoria cache”, fiind apoi preluat de aici la următoarele apariţii ale caracterului.

Pentru o mică ilustrare intermediară, să alegem un caracter cât mai simplu ca formă grafică şi să producem traseul acestuia, folosind charpath şi pathforall:

%!  ex_path.ps
/Symb {/Symbol findfont 1000 scalefont} bind def
% Symb /Encoding get ==    % 190: /arrowhorizex
/str 4 string def  % pentru 'cvs' (transformă în şir numere de max. 4 cifre)
/prst {
    print  % afişează şirul din vârful stivei
    round cvi  % rotunjeşte şi reţine ca întreg, valoarea ajunsă în vârful stivei
    str cvs print  % transformă în şir şi afişează
} bind def
/Prt {prst ( ) prst (\n) print} def  % aplică 'prst' pe abscisă şi pe ordonată

Symb setfont
0 0 moveto  % originea graficului
(\276) true charpath  % 190 = 2*64 + 7*8 + 6 = (\276)
{exch (move: ) Prt}  % fără 'exch', în vârful stivei aveam ordonata (nu abscisa)
    {exch (line: ) Prt}
        {}  % nu-i cazul de 'curveto'
            {(closepath) ==}
                pathforall  % {...}{...}{...}{...} pathforall
vb@Home:~/20mai$ gs -q  ex_path.ps
move: -59 218   % -59 218 moveto
line: 1040 218  % 1040 218 lineto
line: 1040 273  % 1040 273 lineto
line: -59 273   % -59 273 lineto
line: -59 218   % -59 218 lineto
(closepath)   
move: 1000 0    % 1000 0 moveto

Să nu ne cramponăm de faptul că valorile afişate diferă puţin—numai cu cel mult 1050-1040=10, adică cu max. o sutime de unitate—faţă de valorile din fişierul metric "StandardSymbolsPS.afm", reflectate mai sus pe imaginea produsă prin chSketch; pe parcurs, până la tipărirea sau afişarea graficului, intervin şi se cumulează „erori de aproximare” şi anumite ajustări dictate de rezoluţia dispozitivului de ieşire.
Traseul rezultat astfel pentru caracterul de cod 190 "/arrowhorizex" este un dreptunghi cu laturile paralele axelor, creat unind succesiv colţurile prin lineto; el poate fi produs ca atare prin stroke, sau poate fi umplut cu nuanţa curentă de culoare prin fill (desigur, în prealabil trebuie să rescalăm fontul, faţă de mărimea 1000 – folosită mai sus în ideea de a regăsi cumva valorile metrice standard, din fişierul AFM).

Revenind la programul nostru, producem prin charpath graficul caracterului de cod C, prevăzând fill (şi o anumită nuanţă de gri) pentru cazul când WO este false:

    0 0 moveto
    //octch 0 C put 
    //octch true charpath
    WO false eq {0.5 setgray  fill} if

În cazul când WO este true, conturul caracterului va fi trasat (prin stroke) odată cu liniile Lb, Vl şi Vr:

    0 0 moveto
    Ws 0 rlineto  % uneşte originea caracterului cu originea celui care i-ar urma
    Lxs dup Lys moveto Uys lineto  % verticala de mărginire în stânga 
    Uxs dup Lys moveto Uys lineto  % verticala de mărginire în dreapta
    0.05 setlinewidth 0.1 setgray  stroke

Rămâne să adăugăm metricile caracterului; dacă limita inferioară a boxei de încadrare este pozitivă (cum a fost mai sus, cazul caracterului /arrowhorizex), atunci redefinim Lys fixându-i ca valoare 0; apoi, fixăm „punctul curent” ceva mai jos de valoarea Lys (şi la începutul rândului) şi invocăm /wr_metrics:

    Lys 0 gt {/Lys 0 def} if  % pentru a scrie metricile SUB linia de bază
    0 Lys 10 sub moveto
    wr_metrics  % scrie metricile caracterului
} bind def  % încheie definiţia procedurii /chSketch

Cu aceasta, definiţia procedurii chSketch este completă. Cu totul (inclusiv linii albe şi comentarii), fişierul "prchar.ps" măsoară 3175 octeţi (80 de linii).

Un exemplu de folosire; problema includerii

Putem adăuga în fişier un „program principal”, cum am exemplificat deja mai înainte; însă ar fi de preferat să închidem acum fişierul "prchar.ps", urmând să-l „includem” (prin operatorul run) în acele programe PS în care am vrea să folosim chSketch:

%!    % fişier PS nou, "main.ps"
(prchar.ps) run  % "include" fişierul 'prchar.ps'
/SZ 108 def  % redefineşte eventual, /SZ (mărimea graficelor de caractere)
36 680 translate  % originea pentru un prim rând de caractere
gsave
    2 2 scale  % eventual măreşte (pentru rândul curent), graficul şi textul asociat
    /C059-Roman [-1 556 (abreve) 44 -15 543 692] chSketch  % /NewCenturySchlbk-Roman
    100 0 translate  % fixează originea următorului caracter de pe rând
    /P052-Roman [-1 500 (abreve) 32 -12 471 664] chSketch  % /Palatino-Roman
    90 0 translate
    /NimbusMonoPS-Regular [-1 600 (abreve) 67 -16 547 618] 1 chSketch  % /Courier
grestore

Lansând gs main.ps obţinem pe ecran pagina redată mai sus, conţinând rezultatele celor trei invocări chSketch din "main.ps".

Însă includerea unui fişier PS într-un altul este o operaţie care depinde de context şi este de regulă, obstrucţionată (pentru a evita execuţia, în cazul când programul de „inclus” ar fi unul rău-intenţionat); de exemplu, evince main.ps nu ajunge să redea pagina respectivă (produce eroarea "invalidfileaccess"). Putem totuşi să folosim opţiunea "-dNOSAFER", pentru a permite execuţia unui program „străin”, iar ps2pdf -dNOSAFER main.ps conduce la formatul PDF al fişierului, "main.pdf" (care apoi, poate fi accesat şi din evince, de exemplu).

În „partea a IV-a” ne vom ocupa totuşi de catalogarea caracterelor dintr-un acelaşi font, rescriind în acest scop chSketch (de exemplu, nu ar mai fi necesar ca argument al procedurii, numele fontului; recodificarea ar trebui să vizeze un grup de caractere cu "C=-1", în loc de câte un singur caracter).

vezi Cărţile mele (de programare)

docerpro | Prev | Next