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

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

PostScript
2020 jun

Exerciţiu: obţinerea cataloagelor de fonturi

Ne propunem să folosim prchar.ps din [1] pentru a obţine „catalogul metric” al unui font, reflectând graficele şi metricile caracterelor conţinute; nu va mai fi necesar să scriem la fiecare caracter, numele fontului – încât comentăm (prefixând cu %) ultima linie din definiţia /wr_metrics.

Renunţăm la ideea avansată în [1], de a suplimenta cu o procedură de recodificare pe grupuri de caractere (de exemplu câte 128, ca în programul GS lib/prfont.ps de la care am plecat în „partea I”), în loc de câte un singur caracter (practic, viteza de execuţie nu are de suferit); în [1] am adus pe rând caracterele necodificate iniţial, pe o aceeaşi poziţie din tabloul "Encoding" (la indexul 1, ocupat iniţial cu "./notdef").
Dar nu are nici un rost (şi acum vom omite) scrierea codului 1 sub fiecare dintre aceste caractere; modificăm deci, linia din /wr_metrics pe care formulam scrierea codului de caracter (scriem numai codurile celor aflate în tabloul "Encoding" iniţial):

    _num C dup 1 ne {s_num}{pop} ifelse  % scrie codul (dacă ≠1) şi numele caracterului

Pentru verificare şi (mai ales) pentru a aminti lucrurile din [1], reproducem aici reprezentările prin procedura /chSketch din prchar.ps pentru caracterul cu numele /a14 şi codul 45 din fontul "ZapfDingbats" (dar acum nu mai scriem numele fontului) şi respectiv, caracterul /scommaaccent, necodificat (deci nu mai scriem codul), din fontul "NewCenturySchlbk-Roman", în cele două variante, grafic-contur şi grafic-plin:

Afară de aceste două mici modificări în cadrul procedurii /wr_metrics, nu este necesar să mai schimbăm ceva în prchar.ps.

Pregătirea tabelelor şi fişierelor necesare

Mai întâi, să extragem „liniile metrice” din fişierele "*.afm" existente în pachetul (v. [1]) urw-base35. Constituim un subdirector afmT1/ în care copiem fişierele *.afm şi pregătim un fişier executabil "metrics.sh" prin care să extragem liniile metrice:

vb@Home:~/20-iun$ mkdir afmT1
vb@Home:~/20-iun$ cp /usr/share/fonts/type1/urw-base35/*.afm  afmT1/
vb@Home:~/20-iun$ touch metrics.sh ; chmod a+x metrics.sh

Extragem liniile metrice din fişierele "*.afm", în fişierele "*.met", folosind awk prin:

#!/bin/bash  "metrics.sh"
cd afmT1
for fl in $(ls)  # pentru toate fişierele din afmT1/
do
    awk '/^C / {print $2" "$5" ("$8") "$11" "$12" "$13" "$14}' $fl > ${fl%.afm}.met
done
cd ..

Aici, awk selectează liniile din fişierul ".afm" curent care încep cu "C " (şablonul acestora fiind /^C /) şi scrie (cu redirectare într-un fişier de acelaşi nume, dar cu extensia ".met") pentru fiecare linie, câmpurile care ne interesează, separate prin câte un spaţiu; de exemplu, linia din fişierul ".afm" (pe care awk vede 14 câmpuri, separate prin spaţiu):
    C 33 ; WX 333 ; N exclam ; B 3 -15 336 737 ;
devine—ambalând al 8-lea câmp cu paranteze, prin " ("$8" )"—în fişierul ".met" asociat:
    33 333 (exclam) 3 -15 336 737
adică (dacă vom încadra cu paranteze pătrate) exact argumentul cerut de procedura chSketch din prchar.ps, pentru a scrie graficul şi metricile caracterului respectiv. Vom putea obţine „catalogul” fontului, iterând chSketch pe liniile fişierului ".met".

Ne va fi util un tablou PS conţinând numele acestor fişiere (din care vom putea separa numele de fonturi) şi unul care să conţină numărul de linii din fiecare; le putem obţine uşor printr-un program Bash:

#!/bin/bash
cd afmT1
echo "%!"
echo -n "/PS35 ["  # /PS35 [ ... ]
for fl in $(ls *.met)
do
    echo -n "($fl) "  # numele fişierului ".met" curent 
done
echo "] def"
echo -n "/PS35lg ["  # /PS35lg [ ... ]
for fl in $(ls *.met)
do
    echo -n `wc -l < $fl`" "  # nr. de linii din fişierul ".met" curent
done
echo "] def"
cd ..

Redirectăm ieşirea acestui program pe fişierul PSfnt.ps, având în final:

%!PS    "PSfnt.ps"
/PS35 [(C059-BdIta.met) (C059-Bold.met) (C059-Italic.met) (C059-Roman.met) 
(D050000L.met) (NimbusMonoPS-Bold.met) (NimbusMonoPS-BoldItalic.met) 
(NimbusMonoPS-Italic.met) (NimbusMonoPS-Regular.met) (NimbusRoman-Bold.met) 
(NimbusRoman-BoldItalic.met) (NimbusRoman-Italic.met) (NimbusRoman-Regular.met) 
(NimbusSans-Bold.met) (NimbusSans-BoldItalic.met) (NimbusSans-Italic.met) 
(NimbusSansNarrow-BoldOblique.met) (NimbusSansNarrow-Bold.met) 
(NimbusSansNarrow-Oblique.met) (NimbusSansNarrow-Regular.met) 
(NimbusSans-Regular.met) (P052-Bold.met) (P052-BoldItalic.met) (P052-Italic.met) 
(P052-Roman.met) (StandardSymbolsPS.met) (URWBookman-Demi.met) 
(URWBookman-DemiItalic.met) (URWBookman-Light.met) (URWBookman-LightItalic.met) 
(URWGothic-Book.met) (URWGothic-BookOblique.met) (URWGothic-Demi.met) 
(URWGothic-DemiOblique.met) (Z003-MediumItalic.met)]  def
/PS35lg [853 853 853 853 202 855 855 855 855 855 855 855 854 854 854 854 855 855 
855 855 854 854 854 854 854 190 853 853 853 853 853 853 853 853 853 ] def
/Join {  % str1 str2  Join  str1str2  (concatenează două string-uri)
	2 copy length exch length dup 3 1 roll add  % însumează lungimile şirurilor
	string dup dup 5 3 roll exch putinterval  % înscrie în şirul comun, al doilea şir
	3 -1 roll 0 exch putinterval  % înscrie la începutul şirului comun, primul şir
} bind def

Faţă de ceea ce fusese scris de către programul Bash de mai sus, am adăugat definiţia /Join (procedură generală pentru concatenarea a două şiruri PS); vom utiliza /Join pentru a alipi numele directorului afmT1/, cu numele de fişier din tabloul PS35 – ajungând apoi să constituim un obiect PS "file" prin care să putem citi (din directorul „părinte” al lui /afmT1/) liniile fişierului respectiv.

Majoritatea fişierelor aveau câte 855 de linii (dintre care, una singură reprezintă caracterul "./notdef"); dar cam în fiecare, apăreau două-trei linii care corespund unor caractere „nule” (boxa de încadrare indicată în finalul liniei fiind "0 0 0 0", la fel ca pentru "./notdef") – şi am decis să le elimin din fişierele "*.met", rezultând în /PS35lg lungimile (ceva mai variate) redate mai sus.

Constituirea catalogului

Alegem ca suport formatul A4 (8.3×11.7 inch) şi ca mărime a graficelor de caracter SZ=54 (în prchar.ps foloseam SZ=144 – prea mare acum, când vizăm toate caracterele); pe un rând pot încăpea suficient de bine 7 grafice (cu o anumită distanţare fixată, între ele) şi alegem să scriem pe pagină 7 astfel de rânduri. Deci pe o pagină a catalogului vor fi reprezentate 49 de caractere; va trebui să avem în vedere cazul când ultima pagină este incompletă şi cazul când ultimul rând al ei are mai puţin de 7 caractere.

În continuare vom desfăşura „pas cu pas” programul catalog.ps, evidenţiind pe cât se poate (sau se cuvine) logica lucrurilor şi diverse aspecte de programare PS.

„Includem” prin operatorul run, fişierele PS PSfnt.ps şi prchar.ps şi demarăm procedura /buildCat:

%!PS  "catalog.ps"
        (PSfnt.ps) run   % tablourile /PS35, /PS35lg; procedura /Join
        (prchar.ps) run  % /FS, /SZ, /chSketch
    /SZ 54 def  % redefineşte mărimea graficelor (în 'prchar.ps': 144)
/buildCat { 35 mod  % <idf> buildCat  sau  <Opţional> <idf> buildCat
    /idf exch def  % idf = 0..34 indică un font din PS35
    count 1 eq  % Cu <Opţional> se alege 'stroke' (Fără: 'fill'; v. prchar.ps)
        {pop /WO true def} {/WO false def} ifelse

Prin /idf, buildCat va prelua din vârful stivei operanzilor argumentul său principal: un index 0..34 (pentru un element al tabloului PS35) şi urmează să formuleze catalogul fontului corespunzător acelui index, folosind în acest scop chSketch din prchar.ps.
Dar chSketch produce graficul caracterului fie „plin” (cu fill), fie prin contur (cu stroke); pentru a viza ambele variante, am prevăzut pentru buildCat un al doilea argument, „opţional” şi am introdus variabila /WO – cu valoarea true sau false, după cum numărul de valori rămase în stivă (dat de count) după preluarea argumentului „principal” este sau nu, 1. O invocare ca 3 buildCat ar seta WO pe false, producând grafice „pline”; în schimb, 1 3 buildCat (cu două argumente) ar produce contururile graficelor caracterelor (din fontul indicat pe a patra intrare din tabloul PS35).
Obs. Această idee (probabil, „personală”) de montare a unui argument opţional are totuşi un anumit defect (dar care este uşor de ocolit). Dacă vrem grafice pline (invocând buildCat cu un singur argument), iar apoi vrem să invocăm o altă procedură <parg> Proc care primeşte la rândul ei un argument de pe stivă – atunci ar fi greşită înlănţuirea directă a invocărilor (cum se obişnuieşte în PS): <parg> 3 buildCat Proc (greşit, fiindcă buildCat ar consuma cele două argumente din stivă – producând contururi şi nu cum voiam, grafice pline – şi apoi, Proc nu şi-ar mai găsi argumentul). Corectarea este evidentă – argumentele celor două proceduri trebuie separate: 3 buildCat <parg> Proc.
Obs. Bineînţeles că este posibilă şi altă „corectură”, direct în buildCat: dacă după ce se preia primul argument, count este nenul – atunci preluăm efectiv şi următoarea valoare din stivă şi testăm dacă aceasta este aceea pe care am fixat-o în prealabil ca ţinând numai de buildCat (cu grija de a o plasa înapoi pe stivă în cazul contrar şi cu defectul că ar trebui să explicităm cumva şi utilizatorului, acea valoare specială).

Mai departe, înfiinţăm rând pe rând o serie de proceduri „ajutătoare”; unele dintre ele (chiar toate!) ar putea fi localizate (scrise) în afara lui buildCat (eventual, cu o mică dar subtilă modificare, semnalată mai jos), dar am preferat să le plasăm în interiorul acesteia fiindcă angajează idf sau WO („locale” lui buildCat), sau se angajează una într-o alta.

Prin /fMet şi /nrChar vom obţine (folosind get) numele fişierului vizat în tabloul PS35 prin indexul idf şi respectiv (din tabloul PS35lg), numărul de linii ale acestuia:

    /fMet PS35 idf get def  % numele fişierului de metrici (*.met)
    /nrChar PS35lg idf get def  % nr. de linii din fişier

Obs. Puteam să scriem aceste două proceduri şi în afara procedurii buildCat—de exemplu, chiar în fişierul PSfnt.ps care conţine tablourile implicate de ele—dar cu o „mică” modificare: trebuie să încadrăm cu acolade, de exemplu /fMet { PS35 idf get } def – astfel, întâlnind "{...} def", interpretorul doar va salva undeva definiţia respectivă, urmând ca idf să fie căutat doar la momentul execuţiei programului (în timp ce fără acolade, s-ar semnala eroarea "undefined in idf").

Prin /FntName, din şirul adus pe stivă de „apelul” fMet obţinem numele fontului; de exemplu, pentru şirul (P052-Roman.met) căutarea (.) search ne lasă în vârful stivei true (semnalând „succes”), iar pe nivelele următoare ale stivei ne lasă subşirul (P052-Roman) (care precede pe cel căutat, (.)), apoi însuşi subşirul căutat şi în sfârşit, subşirul rămas (met); folosim roll şi pop pentru a schimba ordinea elementelor din stivă şi a elimina pe cele trei de care nu avem nevoie, iar pe şirul rămas în stivă (P052-Roman) aplicăm operatorul de conversie la „nume PS” cvn – obţinând numele fontului, /P052-Roman:

    /FntName {fMet (.) search pop 3 1 roll 
              pop pop cvn} def  % extrage din numele fişierului, numele fontului

Obs. De obicei se foloseşte {...} bind def; întâlnind bind, interpretorul va „înlocui” operatorii existenţi în corpul procedurii prin „pointeri” la codurile executabile asociate lor, scutind căutarea ulterioară a acestora. Dar este suficient să specificăm bind în finalul procedurii buildCat – fiindcă bind este aplicat recursiv, tuturor sub-procedurilor.

Cu /FL ne punem la dispoziţie un obiect PS file, prin care să putem citi liniile din fişierele "*.met"; trebuie să ţinem seama de faptul că aceste fişiere sunt în subdirectorul afmT1/ al directorului care conţine programul catalog.ps, folosind /Join (din PSfnt.ps) pentru a face alipirea necesară:

    /FL (afmT1/) fMet Join (r) file def

Prin /AFM obţinem un tablou cu cele 7 valori de pe linia curent citită prin /FL, din fişier; token interpretează câte un grup de caractere citite rând pe rând din fişier, ca reprezentând una sau alta dintre entităţile lexicale din PS (rezultând în cazul de faţă, 6 numere şi un string), iar astore înglobează valorile respective într-un tablou PS:

    /AFM {7 {FL token pop} repeat 7 array astore} def

Subprocedurile descrise mai sus (/fMet, /nrChar, /FntName, /FL şi /AFM – denumite mai mult sau mai puţin inspirat) ţin toate, de "input" (introducerea şi adaptarea datelor în program); următoarele proceduri ţin de "output", servind pentru formularea şi scrierea rândurilor şi paginilor.

Paginile catalogului sunt formate din câte 7 rânduri, exceptând eventual ultima pagină; fiecare rând este format din 7 „grafice” (exceptând eventual, ultimul rând al catalogului) – anume, graficele de caracter şi înscrisurile adăugate fiecăruia prin procedura /chSketch din prchar.ps.

Să ne amintim că chSketch are ca argumente numele fontului (obţinut mai sus prin /FntName) şi „tabloul metric” al caracterului (dat acum de /AFM) şi are un „argument opţional”, a cărui prezenţă (indicată prin /WO) are ca efect producerea de contururi, în loc de grafice „pline”. Formulăm întâi o procedură ajutătoare, pentru a disocia între cele două cazuri reflectate de /WO:

    /stk_fill {WO {FntName AFM 1 chSketch 75 0 translate}  % grafice cu 'stroke'
                  {FntName AFM chSketch 75 0 translate}    % grafice cu 'fill'
               ifelse} def  % (în funcţie de prezenţa argumentului <Opţional>)

Să observăm că 75 0 translate deplasează originea de la care chSketch tocmai a produs graficul, cu 75 de puncte tipografice spre dreapta; altfel spus, pentru fiecare grafic se rezervă pe orizontală exact 75 de puncte – rezultând implicit, alinierea pe verticală (în cadrul paginii curente) a graficelor (desigur… nu ştiu de ce n-am ales 72, adică exact un inch; un rând de grafice ar fi măsurat atunci exact 7 inch, fiind ceva mai uşor de imaginat –fără calcule suplimentare– pe formatul A4).
Obs. Este drept că numele fontului (care aici este constant) se transmite procedurii /chSketch de fiecare dată când va fi invocată; dar acest defect este de natură „teoretică” (practic, viteza de execuţie nu are de suferit) şi n-am vrut să „repar” /chSketch.

Prin /ROW se produce un rând complet, repetând de 7 ori stk_fill:

    /ROW {7 {stk_fill} repeat} def

Începem fiecare pagină la ordonata TOP=800 (faţă de marginea de jos); rezervăm primul rând pentru ceva titlu sau antet de pagină. /PAG primeşte de pe stivă numărul de rânduri care trebuie scrise pe pagină (va fi 7, exceptând totuşi ultima pagină), coboară TOP cu câte 100 de puncte pentru fiecare nou rând şi scrie prin /ROW rândul curent:

    /TOP 800 def  % ordonata primului rând (posibil vid) de pe pagină
    /PAG { /nrp exch def
        nrp { /TOP TOP 100 sub def  % ordonata rândului următor al paginii
              gsave
                  50 TOP translate ROW  % scrie rândul curent de 7 grafice
              grestore
        } repeat
    } def

Obs. 50 TOP translate ROW înseamnă că rândul curent începe la punctul S(50, TOP_curent); dacă n-am fi ambalat cu gsave şi grestore (prin care se salvează şi se reconstituie „contextul grafic” iniţial, inclusiv originea curentă a sistemului de coordonate), atunci S ar fi fost raportat la punctul lăsat drept „punct curent” de execuţia lui ROW - adică la capătul din dreapta al rândului curent, încât fiecare nou rând ar fi fost scris cu 50 puncte mai la dreapta sfârşitului celui precedent (şi dedesubt, cu câte cele 100 de puncte cu care este coborât TOP la fiecare iteraţie).
Desigur, ar fi fost şi alte posibilităţi (mai complicate decât folosind direct gsave şi grestore) pentru a separa între ele translaţiile orizontale aplicate în ROW, de cele verticale din PAG.

Încheiem seria procedurilor ajutătoare calculând numărul de pagini „întregi”, câte rânduri are ultima pagină (zero dacă este „completă”), câte rânduri complete are ultima pagină (dacă este incompletă) şi câte grafice are ultimul rând din catalog (dacă acesta este incomplet):

    /nPag {nrChar 49 idiv} def  % pagini "întregi" (7x7 grafice)
    /rPag {nrChar 49 mod} def  % rânduri pe ultima pagină (dacă este incompletă)
    /rPagl {rPag 7 idiv} def  % rânduri "întregi" pe ultima pagină (incompletă)
    /rPgl {rPag 7 mod} def  % nenul dacă ultimul rând are sub 7 grafice

Obs. Cu div în loc de idiv trebuia să aplicăm şi cvi, pentru a avea câtul întreg.

Începem „partea executivă” a procedurii buildCat. Întâi înscriem un titlu (numele fontului catalogat şi numărul de caractere); ambalăm scrierea cu gsave şi grestore, fiindcă folosim alte fonturi decât cel catalogat şi în plus, folosim translate:

    gsave
        200 TOP translate 0 0 moveto  % rândul de început al primei pagini
        18 /Times-Italic FS setfont % fontul cu care se scrie titlul
        FntName 30 string cvs show  % scrie numele fontului
        12 /Times-Roman FS setfont  % pentru a specifica şi numărul de caractere
        (  \() show nrChar 4 string cvs show ( caractere\)) show
    grestore

Scriem câte o pagină „completă” (cu 7 rânduri de grafice) invocând PAG, o ejectăm prin operatorul showpage, redefinim TOP=800 (pentru următoarea pagină completă) şi repetăm de câte ori am obţinut mai sus în nPag:

    nPag {7 PAG showpage
          /TOP 800 def} repeat

Dacă rPagl este mai mare ca zero, atunci ştim ca ultima pagină a catalogului este incompletă; scriem rândurile „întregi” ale acesteia:

    rPagl {/TOP TOP 100 sub def
           gsave 50 TOP translate ROW grestore} repeat

Dacă rPgl este mai mare ca zero, înseamnă că ultimul rând al catalogului este incomplet; îl scriem repetând stk_fill de rPgl ori, apoi ejectăm şi ultima pagină:

    50 TOP 100 sub translate
    rPgl {stk_fill} repeat
    showpage  % ejectează ultima pagină
} bind def  % Încheie buildCat ('bind' este aplicat recursiv subprocedurilor)

Cu aceasta, definiţia procedurii buildCat (pe mai puţin de 50 de linii de program) este completă; în principiu, putem închide şi fişierul catalog.ps care o conţine.

Folosirea programului catalog.ps (de la PS, la PDF)

Cea mai banală exploatare a procedurii builCat constă în adăugarea dedesubtul ei a unui „program principal” care să o invoce, ca de exemplu:

% Exemplu de folosire directă (gs catalog.ps):
 3 buildCat  % catalogul fontului 'C059-Roman' (/NewCenturySchlbk-Roman), cu 'fill'
 1 24 buildCat  % apoi, 'P052-Roman' (/Palatino-Roman), cu 'stroke' (contururi)

Executând programul de sub interpretorul Ghostscript (prin gs catalog.ps), obţinem rând pe rând cele câte 18 pagini ale cataloagelor celor două fonturi indicate. Dar acest procedeu direct are în mod inerent un neajuns: după ce se redă pagina curentă (în fereastra grafică deschisă de la bun început de către GS), se afişează la consolă mesajul ">>showpage, press <return> to continue<<" şi apăsând tasta indicată – pagina precedentă „dispare”, fiind înlocuită cu următoarea pagină.

Eliminând „programul principal” de mai sus, putem obţine într-un fişier PS întregul catalog, folosind opţiunea GS -sDEVICE=ps2write şi folosind opţiunea "-c" pentru a invoca buildCat:

gs  -dNOPAUSE -dBATCH -sDEVICE=ps2write -sOutputFile=cat_3.ps  catalog.ps  \
    -c 3 buildCat quit

Documentul "cat_3.ps" rezultat astfel poate fi deschis de exemplu, din evince (obişnuitul "Document Viewer", în Ubuntu-Linux) şi conţine toate cele 18 pagini ale celui de-al patrulea font înregistrat în tabloul PS35.

Este drept că fişierul "cat_3.ps" este cam mare (peste 1.1MB); dar îl putem prelucra mai departe, nu numai reducând dimensiunea de peste 3 ori, dar şi aducându-l la un format de calitate superioară – prin ps2pdf cat_3.ps. Rezultă fişierul "cat_3.pdf" (în format PDF, măsurând sub 290kB), pe care îl şi redăm aici:

Pentru încă o exemplificare (acum în ambele variante – contururi, respectiv grafice pline), alegem a cincea intrare din tabloul PS35 (fontul corespunzător – numit în GS "ZapfDingbats" – având numai 202 caractere, cum vedem în tabloul PS35lg):

gs -q -dNOPAUSE -dBATCH -sDEVICE=ps2write -sOutputFile=ZapfDingbats.ps catalog.ps  \
                                          -c 1 4 buildCat  4 buildCat quit
ps2pdf ZapfDingbats.ps 

Fişierul PDF rezultat (măsurând sub 160kB) cataloghează caracterele fontului indicat, odată (pe primele 5 pagini) cu grafice conturate şi apoi, cu grafice pline.

Putem obţine desigur (chit că nu prea are vreun sens) şi un fişier PDF conţinând cataloagele tuturor celor 35 de fonturi PS de bază: adăugăm în catalog.ps definiţia unui tablou al indecşilor 0..34,

/IDX [0 1 34 {} for] bind def

şi folosim comenzile de mai sus, dar cu "-c IDX {buildCat} forall quit" (şi cu -sOutputFile=all_cat.ps, să zicem); fişierul final "all_cat.pdf" se obţine în aproape un minut şi are 603 pagini (măsurând 8.5MB).

Nu este cazul să ne facem griji privitor la corectitudinea redării graficelor caracterelor din fonturile respective (oare nu cumva, graficul vreunuia nu a putut fi redat – fontul respectiv nefiind încorporat în fişierul PDF – şi a fost înlocuit cu un „grafic vid” ?); graficele respective au fost produse prin charpath (din prchar.ps), încât nu a fost necesară „înglobarea” în PDF a fonturilor de care ţin caracterele respective (inspectând cu pdffonts documentul final, se poate constata că niciunul dintre fonturile catalogate nu nu este menţionat, ca încorporat sau nu, în PDF).
În „partea a V-a” vom formula un catalog mai simplu – folosind nu charpath, ci glyphshow – şi vom vedea că va fi nevoie în acest caz, să forţăm încorporarea în PDF a fontului catalogat.

vezi Cărţile mele (de programare)

docerpro | Prev | Next