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

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

PostScript
2020 jun

Exerciţiu: catalogarea glifelor şi numelor de caracter

Vom constitui un program "glyphCat.ps", prin care să tipărim glifele şi numele de caracter din unul sau altul dintre fonturile PS de bază ("Type 1"); ne interesează acum numai glifele (nu şi metricile asociate şi nici contururile componente, ca în [1]), încât pentru a scrie caracterele după numele lor, vom folosi glyphshow.

Intenţionăm să formulăm pagina fixând un prim rând şi coborând succesiv ordonata acestuia pentru rândurile următoare, până ce se ajunge la baza paginii (ceva mai flexibil decât procedam în [1], unde fixam din start câte rânduri să scriem pe pagină); vom scrie câte un rând de glife (cu mărime de font şi distanţare potrivite), adăugându-i imediat dedesubt, un rând conţinând în aceeaşi ordine numele acestora (scrise cu un font stabilit în prealabil), separate prin spaţiu.

Formulăm întâi obişnuitele „proceduri ajutătoare” (de setat fonturi, de scris un rând de glife, de trecere pe rândul următor, pentru scrierea numelor, etc.); unele dintre acestea ar putea fi „importate” din alte programe PS.

Procedură de ordonare a numelor de caracter

Pentru a facilita căutarea în catalog, pare firesc să aşezăm glifele în ordinea alfabetică a numelor de caracter. O procedură de ordonare ar avea o utilitate generală, încât se cuvine să o tratăm într-un fişier separat, bbsort.ps; este drept că am putea să adoptăm una existentă deja (de exemplu, în GS lib/prfont.ps, de la care am plecat în „partea I”, găsim o procedură /sort foarte eficientă), dar preferăm să modelăm direct o metodă simplă de ordonare (v. Bubble_sort, pentru un pseudocod).

Fiindcă operatorii PS gt sau lt servesc pentru a compara numere sau string-uri (nu şi elemente de tip PS "name"), va trebui să folosim cvs pentru a converti numele de caracter în string (de exemplu, din numele /Abreve obţinem string-ul (Abreve)). Vrem să ordonăm alfabetic indiferent de tipul primei litere (mare, sau mică) din numele de caracter; internalizăm în acest scop procedura /to_lower (prin care, când codul primei litere este sub 97, i se adună 32 – obţinând litera mică omonimă).
Pentru a interschimba între ele două elemente din tabloul supus ordonării, angajăm stiva operanzilor drept intermediar şi folosim put:

%!    bbsort.ps
/sort_cvs {  % <Listă>  sort_cvs  (ordonează <Listă> după şirurile asociate prin 'cvs')
    /Lst exch def
    /nr Lst length def
    /str1 20 string def  % pentru a converti valori din Lst la PS_string
    /str2 20 string def  % (şi a le putea compara alfabetic)
    /to_lower {  % (Abreve) --> (abreve)
        dup dup
        0 get dup 97 lt {32 add 0 exch put} {pop pop} ifelse
    } def
    {  % loop  (repetă până când indexul ultimului "swap" devine 1)  
        /top 0 def  % iniţializează indexul ultimului "swap"
        /nr nr 1 sub def
        % compară elemente vecine, până la indexul ultimului "swap"
        1 1 nr {  % for i1:=1 to nr  
            /i1 exch def
            /i2 i1 1 sub def  % i2 := i1 - 1
            Lst i1 get str1 cvs to_lower  % valorile Lst[i1] şi Lst[i2],
            Lst i2 get str2 cvs to_lower  %     convertite la câte un string
                lt {  % "swap" Lst[i1] <--> Lst[i2], dacă şirurile nu sunt în ordine
                    Lst i1 Lst i2 get  % Lst i1 Lst[i2]  (pe stivă)
                    Lst i2 Lst i1 get  % Lst i2 Lst[i1]  (adaugă în stivă)
                    put  % pune Lst[i1] din vârful stivei, pe locul i2 din Lst
                    put  % pune Lst[i2] salvat în stivă, pe locul i1 din Lst
                    /top i1 def  % reţine indexul acestui ultim "swap"
                } if
        } for
        /nr top def  % se va repeta numai până la indexul ultimului "swap"
        nr 1 le {exit} if  % încheie dacă s-a făcut un singur "swap"
    } loop
} bind def

sort_cvs va putea fi aplicată unui tablou cu elemente de orice tip:

vb@Home:~/20-iun$ gs -q
GS> (bbsort.ps) run
GS> /myar [/sdf /Sdf1 (Abc) 123 1111 /acd /.notdef (Bcd) /b /Q] def
GS> myar sort_cvs
GS> myar ==
[/.notdef 1111 123 (Abc) /acd /b (Bcd) /Q /sdf /Sdf1]

Obs. Lst, nr, to_lower, etc. introduse mai sus în sort_cvs, sunt „variabile globale” şi este posibilă suprapunerea (nedorită, în general) cu variabile ale fişierului PS în care am importa bbsort.ps; puteam evita aceasta dacă le încorporam într-un dicţionar:
sort_cvs { 8 dict begin /Lst exch def /nr ... /str1 ... /top ... end } bind def
Toate cele 8 variabile deveneau astfel, „variabile locale” procedurii.

Obţinerea catalogului glifelor

De obicei, la început se specifică fişierele de importat (prin run), o manieră sintetică de accesare şi scalare a fişierelor-font, precum şi fonturile care vor fi utilizate:

%!   glyphCat.ps
(bbsort.ps) run  % importă /sort_cvs (ordonează alfabetic un tablou)
/fnd_scl {findfont exch scalefont} bind def  % <line-height> </FontName>  fnd_scl
/fontN {11 /Times-Italic fnd_scl} def  % un font obişnuit, pentru numele de caracter
/fontT {16 /Times-Roman fnd_scl} def  % fontul titlului catalogului

La sfârşit vom specifica procedura principală; dar aceasta trebuie imaginată chiar acum, pentru a ne da seama ce sub-proceduri şi variabile ar fi de pus la dispoziţia ei. Imaginăm obţinerea catalogului glifelor unui font prin: <nume_font> glyphList; primind de pe stivă numele fontului, procedura /glyphList (pe care urmează să o definim la sfârşit) va putea seta fontul respectiv (folosind /fnt_scl) şi va putea accesa din dicţionarul acestuia subdicţionarul "CharStrings", ale cărui chei sunt numele glifelor.

Următoarea procedură presupune că numele glifelor au fost scoase pe stivă într-un tablou şi salvează acest tablou în /keys:

/storeNames {/keys exch def} bind def  % tabloul cheilor din /CharStrings

Putem formula imediat (folosind /fontT şi keys length) o procedură pentru scrierea unui titlu, într-o poziţie fixată din exterior (îl vom scrie numai pe prima pagină):

/title {  % titlu (pe prima pagină)
    gsave  % salvează contextul (fontul curent şi "currentpoint")
        currentfont /FontName get  % numele fontului catalogat
        fontT setfont  % comută fontul curent pe Times-Roman (16)
        30 string cvs show ( \() show   % scrie numele fontului catalogat
        keys length 4 string cvs show  % scrie numărul de caractere
        ( caractere\)) show
    grestore  % revine la fontul catalogat (şi poziţia de scriere iniţială)
} bind def

Avem în vedere formatul de pagină "A4" şi putem stabili ca primul rând să înceapă la 1in=72bp faţă de marginea stângă şi 800bp faţă de marginea de jos (pentru formatul "Letter" am înlocui 800 cu 750); prin /newl vom iniţia scrierea unui nou rând (fixând "currentpoint" dedesubtul originii celui curent, la distanţa verticală indicată pe stivă):

/newpag {72 800 translate  0 0 moveto} bind def
/newl {  % newline (<dy> newl)
    /dy exch def
    currentpoint exch  % y x
    pop dy sub 0 exch moveto % y dy | y1=y-dy 0 | 0 y1 | moveto
} bind def

Ne-am propus din start, să scriem câte un rând de glife (distanţate suficient, pentru claritate), însoţit imediat dedesubt de un rând conţinând în aceeaşi ordine, numele acestora (separate cu câte două spaţii); fiindcă unele nume sunt cam lungi (şi vrem să le scriem cu o mărime de caracter obişnuită), stabilim să scriem câte (maximum) 8 glife (respectiv, numele asociate) pe fiecare rând.

Avem nevoie de o zonă de memorie pentru a converti (prin cvs) la "string" numele glifei curente (pregătind scrierea acestuia, prin show) şi de un tablou în care să cumulăm pe rând cele (maximum) 8 nume de scris pe rândul curent:

/nume 20 string def  % pentru conversia la 'string' a numelui de glyph
/arrN 8 array def  % va înregistra succesiv câte 8 nume de caracter

Obs. Dacă lungimea 20 din /nume este totuşi prea mică, interpretorul va furniza un mesaj de eroare (şi atunci, o vom mări); în "Adobe Glyph List" (dicţionarul /AdobeGlyphList specificat în Resource/Init/gs_agl.ps din Ghostscript) putem găsi şi nume foarte lungi, de exemplu: "whitediamondcontainingblacksmalldiamond".

Prin procedura următoare scriem rândul numelor înregistrate în tabloul arrN şi apoi, pregătim scrierea următorului rând de glife (dacă mai încape în pagina curentă – altfel, ejectăm prin /showpage pagina curentă şi iniţializăm o nouă pagină):

/writeNames {  % scrie dedesubtul rândului de glyph-uri, numele acestora 
    16 newl
    gsave  % salvează 'currentfont' şi 'currentpoint'
        fontN setfont  % setează drept 'currentfont', Times-Italic
        arrN {nume cvs show (  ) show} forall  % nume1  nume2  nume3 ...
    grestore  % restaurează vechile 'currentfont' şi 'currentpoint'
    currentpoint exch
    pop neg 700 lt  % trece la un nou rând (dacă încape), sau la o nouă pagină
    {36 newl} {showpage newpag} ifelse
} bind def

Pentru parcurgerea tabloului keys pe secţiuni de câte opt nume, folosim o variabilă de indexare /idx pe care o iniţializăm cu 0 după ce vom fi scris rândul curent de 8 glife şi o incrementăm apoi pe măsură ce parcurgem următoarele 8 elemente din keys:

/id0 {/idx 0 def} def  % index iniţial într-un tablou
/id1 {/idx idx 1 add def} def  % incrementează indexul curent

După fiecare scriere a unui caracter (fie prin show, fie prin glyphshow), în "currentpoint" avem originea la care ar urma să fie scris următorul caracter; deplasând suficient "currentpoint" (ţinând seama de mărimea de caracter aleasă pentru glife), putem aranja ca glifele de acelaşi index 0..7 scrise pe rânduri diferite, să aibă aceeaşi margine stângă în cadrul paginii. Alegem să deplasăm "currentpoint" astfel încât distanţa dintre marginile-stânga a două glife consecutive pe un acelaşi rând să fie de 60 puncte (suficient, având în vedere că în glyphList vom scala fontul indicat la 24 puncte):

/nextpos { % poziţia următorului glyph (rezervăm 60 puncte pentru fiecare)
    currentpoint pop 60 idx 1 add mul exch sub 0 rmoveto} bind def

Prin procedura următoare înscriem numele curent (din keys), în tabloul arrN pe poziţia indicată de idx; dacă s-au completat toate cele 8 poziţii, atunci scriem rândul celor 8 nume (prin /writeNames) şi iniţializăm idx=0 pentru viitorul rând – altfel, avansăm "currentpoint" la marginea stângă următoare (prin /nextpos) şi incrementăm idx:

/put_wr8 {  % memorează şi în final scrie, numele glyph-urilor de pe un rând
    arrN exch  % arrN Ng
    idx exch  % arrN idx Ng
    put  % înscrie Ng pe locul idx curent, în arrN
    idx 7 eq {writeNames id0}  % scrie rândul celor 8 nume, sau 
             {nextpos  id1}    % avansează pe rândul glyph-urilor şi idx++
             ifelse
} bind def

Vom putea scrie rândurile de glife şi de nume, iterând împreună glyphshow şi put_wr8, pe tot cuprinsul tabloului keys (exceptând ultimul rând de glife din catalog, dacă acesta are mai puţin de 8 glife).

Procedura finală (în care ne îngrijim şi de ultimul rând) se poate formula astfel:

/glyphList {  % <font> glyphList  (cataloghează glyph-urile din fontul indicat)
    /Font exch def
    24 Font fnd_scl setfont
    currentfont /CharStrings get 
    {pop} forall  % descarcă procedura (--string--) asociată cheii
    count array astore  % în stivă avem acum tabloul cheilor din /CharString
    storeNames  % salvează tabloul din stivă în /keys
    keys sort_cvs  % ordonează alfabetic numele glyph-urilor
    newpag
    title  % scrie titlul, pe prima pagină
    72 newl
    id0  % iniţializează cu 0 indexul numelor din tabloul arrN
    % scrie câte un rând de glyph-uri şi unul de nume (întorcând eventual pagina)
    keys { dup glyphshow put_wr8 } forall
    idx 7 lt {  % dacă ultimul rând de glyph-uri al ultimei pagini este incomplet
        16 newl % trece pe rândul destinat numelor
        fontN setfont
        arrN 0 idx getinterval {  % selectează numele glyph-urilor rămase
            nume cvs show (  ) show
        } forall  % scrie numele glyph-urilor de pe rândul incomplet
        showpage  % ejectează pagina finală
    } if
} bind def 

Precizăm că fişierul glyphCat.ps (larg desfăşurat mai sus) are sub 80 de linii (3.5kB).

De la PS la PDF

Să considerăm un program "maingly.ps", care (după ce „importă” fişierul glyphCat.ps) să folosească procedura glyphList:

%!    maingly.ps
(glyphCat.ps) run
/Times-Roman glyphList

Putem obţine catalogul în format PDF prin (v. şi [1]):

vb@Home:~/20-iun$ ps2pdf -dNOSAFER  maingly.ps

Dar în fişierul PDF rezultat astfel, foarte multe caractere nu sunt reprezentate decât prin nume (lipsind glifele); în plus, unele caractere sunt redate prin alte glife decât cele existente în fontul catalogat:

Simbolul ₤ de pe al treilea rând de glife (asociat numelui /afii08941) este un simbol monetar (pentru "lire") binecunoscut, dar diferă de simbolul produs prin /afii08941 glyphshow în procedura glyphList invocată mai sus pentru fontul Times-Roman.

Explicaţia acestor neajunsuri constă în faptul că fontul de catalogat nu a fost încorporat în fişierul PDF (ceea ce putem constata folosind pdffonts, sau deschizând fişierul PDF în evince şi accesând meniul File/Properties).
Pentru a forţa încorporarea fonturilor în PDF, putem folosi opţiunea -dEmbedAllFonts (dar trebuie combinată cu -dPDFSETTINGS=/prepress):

 ps2pdf -dNOSAFER -dPDFSETTINGS=/prepress -dEmbedAllFonts=true  maingly.ps

Vedem acum că simbolul pentru /afii08941 (înregistrat în clona "NimbusRoman-Regular" de care putem dispune liber în Ghostscript pentru fontul "Times-Roman") nu este ₤, ci apare format prin alipirea unui "z" cu un "l tăiat" (al patrulea caracter de pe linia a treia, din prima pagină a catalogului obţinut). Putem selecta şi copia caracterul respectiv (direct de pe PDF-ul redat mai sus) şi pastându-l aici, obţinem simbolul obişnuit pentru „lira sterlină”: ₤. Dacă derulăm catalogul până la numele care încep cu litera "l" (pagina 4, jos) găsim numele "/lira", având ca simbol tot combinaţia de "z" şi "l tăiat"; dacă derulăm până la litera "s" (pagina 6, jos), găsim numele /sterling, cu simbolul £ (asemănător celui obişnuit pentru „liră”).

Putem face desigur, o ultimă probă (pentru a vedea de exemplu, dacă este cazul să adoptăm o procedură de ordonare mai eficientă decât cea din bbsort.ps) – iterând glyphList pe fonturile „principale” înregistrate în fişierul Fontmap.GS din Ghostscript:

%!    maingly.ps
(glyphCat.ps) run
    [/AvantGarde-Book /Bookman-Demi /Courier /Helvetica 
     /NewCenturySchlbk-Roman /Palatino-Roman /Symbol 
     /Times-Roman /ZapfChancery-MediumItalic /ZapfDingbats]
{glyphList} forall

Fişierul PDF obţinut (în mai puţin de 15 secunde, pe sistemul meu) prin comanda ps2pdf formulată mai sus, are 76 de pagini (1.1MB), conţinând în ordine alfabetică numele şi glifele corespunzătoare, pentru toate fonturile indicate; n-ar fi greu de bănuit că (exceptând /Symbol şi ZapfDingbats) acestea conţin aceleaşi 1004 nume de caracter (diferind desigur, glifele asociate), selectate dintre cele vreo 4200 anticipate de Ghostscript (9.26) în dicţionarul iniţial /AdobeGlyphList.

Vedem acum, că probabil era mai bine să fi procedat altfel: să luăm pe rând numele înscrise în "Adobe Glyph List" şi să le identificăm în dicţionarele "CharStrings" ale fonturilor PS de bază, scriind numele respectiv şi adăugând (când există) glifele asociate în fiecare font (această idee poate demara vreun alt „exerciţiu” şi desigur, nu ne vom putea lăuda niciodată că „am terminat”).

vezi Cărţile mele (de programare)

docerpro | Prev | Next