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

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

PostScript
2020 apr

[1] BLUEBOOK (PostScript language Tutorial and Cookbook, Adobe Systems, 1985)

Cum citeşti, când citeşti

Înveţi să citeşti şi să scrii mai ales după ce vei fi terminat cu obligaţiile şcolare, dacă ai un anumit interes sau aplecare. Citeşti, reciteşti, scrii şi rescrii (până la urmă parcă se confundă: citeşti) folosind anumite instrumente, inclusiv "Copy&Paste".

„Citeşti” o partidă de şah dintr-o carte sau o revistă de şah—ai nevoie de obicei de o tablă de şah pe care să aşezi (sau să „pastezi”) mutările; ca să citeşti o carte de teorie muzicală, ai nevoie probabil şi de vreun instrument muzical pe care să exersezi fragmentele de partitură exemplificate în cartea respectivă; ca să citeşti cu folos o carte de bucate, îţi trebuie o masă de bucătărie şi anumite instrumente şi ingrediente, pentru a putea ”exersa” textul vreunei reţete culinare.

În cărţile de programare întâlnim fireşte, fragmente (şi reţete) de cod-sursă; unele dintre acestea merită să le şi tastezi, măcar pentru a vedea concret „ce fac”; unele sunt chiar interesante şi pot fi instructive dacă le judeci şi le experimentezi.
Dacă dispui de cartea respectivă în format PDF, atunci fireşte că te poţi scuti de a mai tasta linie după linie fragmentul de cod devenit interesant: îl selectezi, îl copiezi de pe ecran (prin Ctrl+C) şi îl aduci (prin Ctrl+V) în propriul editor de text.
Poţi constata uneori, că textul sau programul obţinut astfel „nu merge” (sau că baclavaua buchisită după reţetă, miroase a ceapă)—dar degeaba dai vina pe carte şi pe autor … de vină sunt instrumentele; alte proprietăţi avea untul (şi râşniţa) acum 50 de ani, când a fost scrisă cartea din care „citeşti” acum reţeta.

"Copy and Paste" şi… execută

Recitind [1]—după unele experienţe în LaTeX cu fonturile—am ajuns la "Ch.9 More Fonts" şi probabil că acum am fost mai pregătit să iau aminte la programul din §9.2 "Character Encoding" (din secţiunea "Putting Codes Into Strings"):

Bineînţeles că am citit cu destulă atenţie, aceste pagini; am sesizat uşor o mică „greşală tipografică” (neimportantă, fiind uşor de corectat), dar am văzut şi unele aspecte de îndreptat, sau de luat în seamă. Aşadar, aş avea de completat sau de rescris programul respectiv – încât l-am copiat de pe ecran în fişierul "putCodes.ps" şi în primul rând, am şters paranteza rotundă din faţa termenului /prt-n (în PS parantezele rotunde definesc un şir de caractere şi "(" nu are ce căuta în faţa unei definiţii de procedură); apoi, am verificat totuşi programul (nu cumva… miroase a ceapă ⁈), lansându-l din Ghostscriptnu funcţionează (dar nu mai redau aici mesajul de eroare—bineînţeles, greu digerabil—emis de GS).

Pot recunoaşte acum că mi-a luat ceva timp ca să înţeleg (cum arăt mai jos, vizând mai târziu şi elemente de programare în PS) de ce nu funcţionează: de vină sunt… fonturile; explicaţia finală este foarte simplă: "Copy&Paste" mi-a adus din [1] şi caractere specifice fonturilor încorporate în fişierul PDF respectiv, iar unele dintre acestea (aş pomeni de prin alte împrejurări, cratima, minus şi "1") sunt nerecunoscute sau sunt altfel mapate, pe sistemul propriu (în care am de exploatat programul copiat). Pe scurt, caracterele „vechi” nu corespund neapărat cu cele „noi”; în plus să zicem, în [1] programele vizau cu precădere interpretorul de PS al unei imprimante, iar acum folosim un instrument mai nou faţă de [1] (interpretorul GS).

Citirea programului

În loc să executăm mecanic programul, imediat ce l-am copiat—să-l citim mai bine: am simplifica unele denumiri (precum "PrintCodeandChar"), am reorganiza blocurile de cod (având alte gusturi decât ar fi fost când a apărut [1]) şi în plus, am adăuga comentarii explicative (compensând cumva, desprinderea din contextul explicativ al cărţii). Cam aşa l-am „reciti” în vederea completării pe care o intenţionăm, dacă am şti că programul „merge”; însă pentru a lămuri de ce „nu merge”—trebuie doar să simplificăm lucrurile (rezumând cumva, „partea executorie” a programului).

Ignorăm deocamdată procedura /newline invocată în finalul lui "PrintCodeandChar" şi reducem for la o singură iteraţie, alegând să producem drept "Code" 162 şi drept "andChar" caracterul '¢' asociat acestuia (cum se vede pe coloana de rezultate redată pe marginea stângă a paginii 92, reproduse mai sus):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
%!PS-Adobe-2.0
/Times-Roman findfont 10 scalefont setfont
/char 1 string def
/nstr 3 string def
/prt-n {nstr cvs show} def
/prtchar {char 0 3 −1 roll put char show} def
/PrintCodeandChar {dup prt-n ( ) show prtchar} def  % fără 'newline' în final
% ---------- Testare ----------  (am redus "partea executorie" iniţială)
72 600 moveto
162 PrintCodeandChar  % Ar trebui să scrie: 162 ¢

Subliniem că am păstrat caracterele iniţiale (aduse prin Copy&Paste); redarea „colorată” de aici—aplicând pygmentize fişierului "putCodes.ps"—evidenţiază deja că '0' şi '3' pe de o parte şi '−1' pe de altă parte (redate prin culori diferite pe linia 6), ar fi categorii sintactice diferite (ceea ce este fals: toate trei ţin de număr şi în mod normal, ar fi colorate la fel)…

Acum GS ne sugerează mai bine motivul neexecutării programului:

vb@Home:~/20mar$ gs -q putCodes.ps
Error: /undefined in �
Operand stack:
   162   (\000)   0   3
Execution stack:  ... ... ...

Definiţiile din liniile 2–7 au trecut cu bine—altfel, eram atenţionaţi de vreo greşală de sintaxă (de exemplu, dacă în linia 3 am fi lăsat "(/char" cum era în textul iniţial, atunci primeam un mesaj de /syntaxerror); deci eroarea semnalată (/undefined) vizează „partea executabilă” a programului, deci de fapt eroarea este provocată de execuţia liniei 10 (linia 9 este dintre cele foarte obişnuite în PS şi nu are de ce să nu „treacă”).

Execuţia liniei 10 se poate judeca expandând succesiv procedurile implicate:
162 PrintCodeandChar % depune 162 pe stivă ("Operand stack") şi invocă PrintCodeandChar
162 dup prt-n ( ) show prtchar % am expandat PrintCodeandChar, pe un prim nivel
dup adaugă în stivă valoarea existentă în vârful ei (rezultând 162 162 prt-n ...) şi se poate verifica direct în GS, că 162 prt-n scrie în pagina curentă (la poziţia definită prin linia 9) valoarea 162 (preluată şi în final eliminată din vârful stivei)—încât după aceasta, contextul de execuţie revine la: 162 ( ) show prtchar. În PS, ( ) reprezintă un şir constituit din caracterul "spaţiu"; show scrie acest şir (preluat şi în final eliminat din vârful stivei) la poziţia curentă din pagină (deci imediat după "162", scris anterior)—încât contextul de execuţie rămas este:
162 prtchar % sau, expandând:
162 char 0 3 −1 roll put char show
În stivă avem acum trei valori: 162, char (care conform definiţiei din linia 2 este un şir constituit dintr-un singur caracter) şi 0; prin 3 -1 roll, aceste trei elemente ar fi „rotite” în stivă, în ordinea char 0 162; iar apoi, put ar înscrie valoarea din vârful stivei (162) la indexul 0 în şirul char (şi în final, char show ar scrie la poziţia curentă din pagină caracterul de cod 162, tocmai adus pe stivă în char).
Dar execuţia lui roll eşuează, cum se vede în mesajul de eroare pe care l-am reprodus mai sus: în "Operand stack" avem 0 şi 3, dar nu şi valoarea −1, necesară pentru roll — semnul '−' din faţa lui '1' o fi fost "minus" în cartea de unde am decupat secvenţa de cod, dar pe sistemul meu acum, el nu este recunoscut ca atare.

Înlocuind '−1' de pe linia 6 prin '-1', constatăm că programul funcţionează; reinserând în program definiţia /newline şi înlocuind liniile 8–10 cu secvenţa de sub "Begin Program" din secvenţa de cod iniţială—obţinem o pagină conţinând coloanele de rezultate din marginea stângă a paginii 92 din [1].

Recitim şi rescriem lucrurile, pas cu pas

Motivul obişnuit pentru care te apuci să rescrii un program citit dintr-o carte este cam acelaşi pentru care te apuci să rezolvi exerciţii la sfârşitul unui capitol dintr-un manual uzual – pentru a consolida cunoştinţele dobândite şi a învăţa să le foloseşti. Dar poţi ajunge în timp şi la motivaţii suplimentare şi de altă calitate – depăşind sintagma comună „păi aşa am învăţat la şcoală”; contează—mai mult decât „la şcoală”—până şi cum „faci” literele sau cifrele: "−1" nu-i ca şi "-1" (cum am evidenţiat şi mai sus); "A" nu-i chiar la fel ca "A", etc.; contează şi mai mult, să legi lucrurile între ele, mereu.

"162 ¢" redă fără nicio distincţie (exceptând separarea prin spaţiu), două entităţi diferite: un cod de caracter şi însuşi acel caracter (mai precis, forma grafică asociată acestuia în fontul respectiv); era mai potrivită o exprimare ca "162 ¢" (că în general, pentru a reda o coloană de numere este recomandabil să se folosească un font „monospaţial”), sau poate mai bine "162 ¢" (mărind puţin, forma grafică).
Sau poate şi mai bine: "162 ¢ cent", unde cent este „numele caracterului”—regăsit şi în alte fonturi, poate la alt index decât 162 şi poate cu altă formă grafică, dar păstrând „semnificaţia” caracterului (de exemplu, numele convenţional "literaA" sau mai scurt "A" – sau ca operator PS, "/A" – poate indica forma grafică 'A' dintr-un font „drept”, ca şi forma 'A' dintr-un font italic, sau forma 'A' dintr-un font monospaţial, etc.).

Schimbarea fontului

Prin urmare s-ar cuveni să implicăm două fonturi (sau trei, dacă ne gândim şi la un „antet” independent de rezultatele produse de program); să prevedem atunci o procedură prin care GS să activeze fontul care îi este indicat la un moment sau altul:

/F {findfont exch scalefont setfont} bind def

De exemplu, 16 /Times-Roman F revine – expandând F – la:
16 /Times-Roman findfont exch scalefont setfont
Procedura findfont caută într-un director (sau într-un dicţionar) de fonturi cheia "Times-Roman" indicată în vârful stivei operanzilor, o găseşte (altfel s-ar furniza un mesaj de eroare) şi plasează pe stivă structura de date (de tip dicţionar) asociată cheii respective – încât apoi, contextul de execuţie devine:
16 d_TR exch scalefont setfont % 'd_TR' este o copie a dicţionarului font
Este de subliniat că findfont depune pe stivă chiar dicţionarul asociat cheii indicate şi nu doar un „pointer” la acesta; se procedează aşa pentru a permite utilizatorului să scaleze (faţă de măsura standard iniţială) formele grafice din fontul respectiv.
Operatorul exch comută între ele cele două elemente din vârful stivei:
d_TR 16 scalefont setfont
Acum, scalefont măreşte caracterele din d_TR la 16 puncte.
Mai rămâne de executat:
d_TR setfont
prin care se fixează fontul reprezentat prin dicţionarul din vârful stivei drept „fontul curent” – cel utilizat de show pentru a trasa forma grafică a caracterelor dintr-un şir.

Se vede că „activarea” unui font este o operaţie complexă şi costisitoare; se cuvine să evităm să o repetăm de multe ori, în cursul programului. În cazul nostru, avem de scris codul caracterului cu un font (o să alegem Courier) şi însuşi caracterul, cu un alt font (cum am exemplificat mai sus, 162 ¢) şi de repetat aceasta pentru fiecare cod; dar avem de evitat dacă se poate, o aşa înlănţuire de operaţii:
{ activează Courier şi scrie codul curent;
activează celălalt font şi scrie caracterul asociat codului;
} repetă pentru următorul cod;
„se poate” însemnând desigur că operaţiile implicate de „evitare”, sunt mai puţin costisitoare decât cele aferente înlănţuirii de activări menţionate.

Evităm să ne aventurăm şi vom folosi totuşi înlănţuirea de operaţii menţionată mai sus, observând un binevenit amendament: putem scurta operaţiile necesare, ocolind activarea „de la capăt”, pentru fiecare cod, a fonturilor respective. Mai precis, putem evita să invocăm de fiecare dată findfont şi scalefont, procedând astfel: rezumăm procedura /F la aceste două operaţii (omiţând setfont final) şi „salvăm” sub o anumită cheie, dicţionarul rezultat aplicând această procedură pentru un font precizat; dicţionarul salvat conţine caracterele mărite deja, încât o viitoare „activare” a fontului va reveni (mult mai simplu) la plasarea pe stivă a cheii prin care se accesează dicţionarul respectiv şi invocarea operatorului setfont:

/F {findfont exch scalefont} bind def
/f_cod 12 /Courier F def  % salvează sub cheia 'f_cod', dicţionarul lui Courier-12
% Exemplu:
f_cod setfont  % activează Courier-12
72 700 moveto  % poziţia curentă de scriere
(162) show  % scrie '162' (cu fontul Courier, mărimea 12)

Precizăm că ideea acestui „amendament”—important, fiindcă evită repetarea operaţiilor costisitoare findfont şi scalefont când avem de schimbat fontul curent cu un altul—vine tot din [1] (Ch.5 Printing Text).

Nu cod → grafic, ci de fapt cod → nume → grafic!

În esenţă, procedura prtchar din programul iniţial (v. mai sus, linia 6 din "putCodes.ps") modelează scrierea unui caracter cam aşa: codul caracterului (cu valori 0..255) este transformat în şir „binar” de lungime 1 şi este pasat astfel, operatorului show.
Dar se cuvine să aflăm mai precis (nu neapărat tot din [1]) şi ce face show: în esenţă, accesează tabloul de sub cheia Encoding din dicţionarul fontului curent, extrăgând numele înscris la indexul dat de şirul binar primit de pe stivă; foloseşte numele găsit astfel pentru a depista undeva—pentru fonturile „de bază”, în dicţionarul CharStrings conţinut în dicţionarul fontului—procedura de executat pentru a produce graficul caracterului.

Ne aşteptam poate la un procedeu direct, cod → grafic; iată că de fapt, se procedează mai complicat: cod → nume → grafic; graficul este determinat de nume şi nu direct de cod – creând de exemplu, posibilitatea de a folosi în descrierea graficului unui caracter, grafice ale altor caractere (invocându-le prin nume-le asociat; de exemplu, probabil că /Scedilla invocă /S şi /cedilla pentru a descrie 'Ş').

Prin următorul mic experiment producem un fişier conţinând tabloul "Encoding", precum şi cheile din dicţionarul "CharString" (nu şi procedurile de trasare asociate cheilor, acestea având o compactare specială, „ascunsă” sub numele generic "-string-"), pentru fontul Times-Roman. Plecăm de la următorul program ("Enc_ChS.ps"):

% --- Enc_ChS.ps ---
/F {findfont exch scalefont} bind def
/f_char 12 /Times-Roman F def
f_char /Encoding get  % tabloul numelor din fontul Times-Roman
pstack  % afişează conţinutul stivei (tabloul "Encoding")
f_char /CharStrings get { pop == } forall  % cheile din dicţionarul "CharStrings"

Executăm acest program prin GS, redirectând ieşirea pe un fişier (în loc de ecran):

vb@Home:~/20mar$ gs -q -dNODISPLAY -dBATCH Enc_ChS.ps > TR.Enc.ChS

Fişierul rezultat "TR.Enc.ChS" conţine la început tabloul "Encoding", conţinând 256 de nume de caractere (îl redăm selectiv, subliniind câteva nume semnificative):

[/.notdef ... /.notdef /space /exclam /quotedbl /numbersign /dollar /percent /ampersand
/quoteright /parenleft /parenright /asterisk /plus /comma /hyphen /period /slash
/zero /one /two ... /nine /colon /semicolon /less /equal /greater /question /at
/A /B /C /D /E ... /Z /bracketleft /backslash ... /a /b /c ... /z /braceleft
/bar /braceright /asciitilde /.notdef ... /.notdef /exclamdown /cent /sterling
/fraction ... /fi /fl ... /oe /germandbls /.notdef /.notdef /.notdef /.notdef]

Dedesubtul acestora avem cheile din dicţionarul "CharStrings", în număr de 1004:

/union /ecaron /yericyrillic /heart
...
/K /logicalnotreversed /a
/integraltp /uni021A /w /Eta
...
/spade /cent /multiply ... /.notdef
...
/uni0493 
/A
/Acyrillic
/icyrillic

107 nume din "Encodings" (între care, primele 31) sunt /.notdef şi în "CharStrings" acestui nume îi este asociată o procedură pentru a trasa un simbol cu semnificaţia de "caracter nedefinit" pentru fontul curent (de obicei, un spaţiu sau un „semnul întrebării”). Tabloul "Encoding" se poate cumva modifica prin program, încât putem face ca show să scrie şi acele şiruri care conţin caractere al căror nume figurează în "CharStrings", dar lipsesc din tabloul "Encodings" iniţial (am putea înlocui /.notdef-uri de la anumiţi indecşi din acest tablou, cu numele respective); avem astfel, încă un avantaj—cel mai important, probabil—al procedeului cod → nume → grafic.

[1] vizează prima versiune a limbajului PostScript; în PS2 (PS "Level 2") apare şi operatorul glyphshow, care produce graficul unui caracter ("glyph") pe baza nume-lui (şi nu plecând de la cod, cum face show); de exemplu, /A glyphshow ar lansa procedura care produce graficul literei "A", din cadrul fontului curent; /cent glyphshow ar trasa caracterul "¢" şi pentru alt exemplu, /uni2126 glyphshow ar produce graficul "Ω".
Este de observat că ideea producerii caracterului pe baza numelui apare şi în alte părţi; de exemplu, pentru a scrie "Ω" mai sus, în HTML, am folosit "Ω".

(cod, grafic, nume) pentru un caracter, în PS2

Angajând cele de mai sus, următorul program (de testare) produce pe ecran:

Fiind vorba de un singur caracter, am scalat la dimensiuni mari (cum se vede şi pe imaginea redată 100% mai sus), cele trei fonturi (alese dintre cele „de bază”) folosite:

%!PS-Adobe-2.0
% --- testChar.ps --- Code Char Name ---
/F {findfont exch scalefont} bind def
/f_cod 16 /Courier F def
/f_char 24 /Times-Roman F def
/f_sans 16 /Helvetica-Oblique F def

72 700 moveto  % poziţia iniţială de scriere

f_cod setfont  % font curent: Courier/16
(162 ) show  % scrie '162 '

/buff 50 string def  % pentru "nume-caracter"
f_char /Encoding get  % aduce pe stivă tabloul numelor din fontul Times-Roman
162 get buff cvs dup  % numele caracterului de cod 162 (dublat apoi în stivă) 
f_char setfont  % font curent: Times-Roman/24
cvn glyphshow  % trasează graficul (caracterul) asociat numelui găsit
(  ) show  % scrie apoi două spaţii
f_sans setfont  % font curent: Helvetica-Oblique/16
show  % scrie numele asociat caracterului

Linia 162 get buff cvs dup ar avea această explicaţie: se găseşte (prin get) numele de la indexul 162 din tabelul "Encoding" depus pe stivă prin linia precedentă şi numele respectiv, "/cent" este depus pe stivă; se adaugă în stivă şirul buff, constituit din 50 de coduri binare "/000" şi se aplică cvs – prin care "/cent" este transformat în subşir (cent) al şirului buff; după dup – pe stivă avem şirul (cent) şi încă o dată, (cent).
Pe liniile următoare, primul şir (cent) din stivă este pasat lui glyphshow (înscriind graficul caracterului numit "cent", după comutarea pe fontul Times-Roman); şirul (cent) rămas după aceea în stivă este pasat lui show, încât este scris (după comutarea pe fontul Helvetica-Oblique) şi numele caracterului.

După această testare pentru un singur caracter, putem reveni la dimensiuni obişnuite pentru fonturi, putem completa cu o procedură /newline şi folosind for (ca în programul iniţial) – putem tabela liniile cod-grafic-nume pentru toate cele 256 de nume înscrise în tabelul "Encoding". Am regăsi astfel şi tabelul de rezultate de la pag. 92, din [1] şi am putea zice că exerciţiul nostru de "recitire şi rescriere" este încheiat (cu satisfacţia că am reuşit cumva „să legăm lucrurile între ele”).

Dar de fapt, foarte rar este adevărat că „am terminat” ceva (poate doar şcoala); de exemplu în cazul de aici, parcă nu-i de omis faptul constatat mai sus că sunt nu 256 de caractere (cu care vom fi terminat mai sus), ci vreo mie… În plus, fiecare caracter are caracteristici dimensionale specifice fontului (lăţime, înălţime, adâncime)—servind pentru poziţionarea graficului respectiv pe rândul curent al paginii (şi a acestuia, pe pagină)—şi dacă vrem ceva „tabel de caractere” (cum ne şi propunem, în partea a II-a), atunci şi acestea ar trebui cumva specificate (pe lângă cod, nume şi grafic).

vezi Cărţile mele (de programare)

docerpro | Prev | Next