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

Aspecte de programare în PostScript - partea întâia

PostScript
2019 may

Elipsa $\mathcal{E}$ este tangentă axei semicardioidei $\mathcal{K}$ în nodul acesteia, are focarele $K_t$ şi $K_{1-t}$ situate pe $\mathcal{K}$, iar centrul ei $C_t$ este situat pe arcul de parabolă $\mathcal{P}$ (unde $t\in[0,1]$ generează punctele lui $\mathcal{K}$ conform parametrizării din [1]):

Anterior am imaginat lucrurile folosind MetaPost sau R; acum folosim (direct!) PostScript, având (măcar teoretic) două avantaje: scurtarea cam la jumătate, a fişierului care conţine descrierea figurii şi asigurarea unei calităţi superioare a redării pe ecran sau pe hârtie (nedepinzând de scalare sau de rezoluţie). De fapt, avem un pretext nobil pentru a experimenta şi a prezenta diverse aspecte de programare în limbajul PostScript (ei da!… un pretext nobil; obiceiul "Hello world!", conform căruia limbajul PS este prezentat plecând de la construcţia unui pătrat - este deja desuet).

Ca şi în cazul când am folosit MetaPost, avem de ales între a contura curbele "punct cu punct", sau prin curbe Bézier (cum ne propunem într-o parte viitoare). Ca şi în cazul MetaPost, ne vom confrunta cu problema adăugării pe figură a etichetelor (cu "font matematic") şi va trebui să implicăm anumite programe utilitare auxiliare.

1. Componentele programului

Pentru contextul matematic al figurii (nu-i şi acesta, o "componentă"?) se poate vedea [1], [2], etc.; aici avem de discutat aspectele de programare.

Programul PostScript care a produs figura postată mai sus este compus astfel:

— fişierul principal "grafic.eps", prin care se generează şi se trasează curbele (elipsa, cardioida şi parabola), segmentele şi arcele importante;

— un anumit număr de fişiere EPS asociate etichetelor matematice pe care vrem să le plasăm pe grafic - fişiere pe care le vom obţine "automat", printr-un script Bash care iterează pe etichetele respective un anumit şablon de fişier LaTeX, invocând compilatorul latex şi programul dvips;

— fişierul "pack.eps" care încapsulează în final, fişierele EPS menţionate mai sus; pasându-l programului ps2pdf (care angajează de fapt interpretorul gs) rezultă fişierul PDF care a fost vizualizat grafic mai sus.

Limbajul PostScript serveşte pentru a descrie o pagină; de regulă, imprimanta va ejecta pagina rezultată prin interpretarea fişierului PS care i-a fost transmis. EPS rezolvă problema încapsulării de fişiere PS, permiţând includerea în siguranţă (fără ejectare şi fără conflicte între parametrii grafici) a unor pagini PS într-o alta. Pentru a respecta formatul EPS, mai întâi declarăm la începutul fişierelor noastre:

%!PS-Adobe-2.0 EPSF-2.0
%%BoundingBox: 75 80 320 248

Vom arăta la momentul potrivit, cum obţinem valorile necesare pentru BoundingBox şi cum le folosim în vederea încapsulării finale a fişierelor respective.

2. Discuţie despre operatori, variabile şi transformări,
pe seama producerii elipsei dintr-un cerc

Definim întâi o procedură pentru a obţine $\mathcal{E}$, o elipsă tangentă în $O$ axei $Ox$, având centrul $C_t$ şi semiaxa mică $b$ date, cu semiaxa mare fixată $a=1$ şi astfel încât panta axei mari este $\mathrm{tg}\,\sqrt{b/a}\,$:

/ellipse {  % tangentă în O axei Ox, cu semiaxele şi centrul date
    /Sb exch def  % Preia din stivă semiaxa mică, b. Semiaxa mare este fixată, a=1.
    /yC exch def  % coordonatele centrului (preluate pe rând, din stiva operanzilor)
    /xC exch def
    /savematrix matrix currentmatrix def % salvează CTM
    xC yC translate  % translatează în centrul elipsei
    Sb sqrt 1 atan rotate  % roteşte cu arctg(SQRT(b/a))
    1 Sb scale  % scalează orizontal cu a=1 şi vertical cu b
    0 0 1 0 360 arc  % "cerc" cu centrul (0,0), "raza" 1 (=a pe Ox şi =b pe Oy)
    savematrix setmatrix  % restaurează CTM ("current transform matrix")
} bind def

În limbajele uzuale definiţia de mai sus ar arăta cam aşa: ellipse(xC, yC, Sb) { ... }; un apel ca ellipse(5, 7, 0.75) creează în memorie un "cadru-stivă" în care se depun valorile 5, 7 şi respectiv 0.75 şi apoi pune în execuţie blocul de instrucţiuni identificat prin "ellipse"; în interiorul acestuia, variabilele locale 'xC', etc. vor fi asignate cu valorile transmise la apel în cadrul-stivă; la încheierea executării codului respectiv, cadrul-stivă asociat la apel va fi eliminat şi controlul va reveni programului apelant.

Spre deosebire de limbajele obişnuite - în care "cadrele-stivă" sunt asociate în mod implicit şi sunt invizibile - în PostScript stiva are o natură explicită şi sunt prevăzute şi menţinute pe parcursul execuţiei mai multe stive, între care: o stivă pentru operanzi, una pentru operatori sau proceduri şi o stivă (sau "dicţionar") de fonturi.

Definiţia de mai sus este depusă întâi, termen după termen, pe stiva operanzilor: /ellipse { ... } şi când se va întâlni apoi def, interpretorul va proceda astfel: va accesa dicţionarul operatorilor ("stiva operatorilor", cum i-am zis mai sus) şi va căuta aici termenul "ellipse"; dacă acesta nu există, atunci va crea o nouă intrare de forma "cheie -> valoare", punând drept "cheie" numele ellipse şi drept "valoare" secvenţa de instrucţiuni cuprinsă între "{" şi "}".
De aici încolo, când va fi întâlnit ulterior în programul curent, termenul "ellipse" va fi interpretat ca "pointer" la secvenţa de instrucţiuni asociată astfel în dicţionarul operatorilor (conducând la executarea acestor instrucţiuni); dar aceasta nu scuteşte etapa prealabilă, de căutare a termenului întâlnit în dicţionarul operatorilor - cu această excepţie: dacă înainte de def este întâlnit bind (ca şi în cazul de faţă), atunci operatorii PS predefiniţi întâlniţi în spate sunt înlocuiţi imediat prin "pointeri" la codul asociat lor ca "valoare" în dicţionarul operatorilor (scutind în acest caz, căutarea ulterioară în dicţionar; se accelerează astfel, execuţia procedurii în care sunt invocaţi).

Dacă după definiţia de mai sus ar urma o invocare ca de exemplu 5 7 0.75 ellipse, interpretorul ar depista cheia 'ellipse' în dicţionarul operatorilor şi ar începe să execute instrucţiunile care i-au fost asociate; primele trei instrucţiuni, /Sb exch def etc., sunt interpretate conform mecanismului general descris mai sus, dar cu o anumită modificare (de observat că acum lipsesc acoladele care încadrează de obicei blocul de instrucţiuni al procedurii): se înfiinţează în dicţionarul operatorilor câte o nouă cheie ("Sb" etc.), căreia i se asociază ca "valoare" nu pointerul la codul operatorului predefinit exch (ca în cazul când s-ar fi întâlnit {exch}), ci efectul executării acestuia urmată de descărcarea din stiva operanzilor determinată de operatorul final def. Sunt modelate astfel atribuirile uzuale, Sb = 0.75, etc.

Ne putem clarifica mecanismele folosite în PostScript experimentând în sesiuni de lucru cu Ghostscript. De exemplu, să introducem pe stivă coordonatele unui punct, (5, 7) şi să imităm mecanismul de "atribuire" tocmai descris pentru a crea o variabilă y legată la ordonata punctului (modelând atribuirea uzuală y=7):

vb@Home:~/5mai$ gs -q
GS> 5 7 /y
GS<3> pstack  % afişează conţinutul stivei, începând din vârful acesteia
/y
7
5
GS<3> exch pstack  % 'exch' schimbă între ele primele două valori din stivă
7
/y
5
GS<3> def pstack  % 'def' mută în stiva operatorilor cheia 'y', asociindu-i valoarea 7
5  % în stivă a rămas doar abscisa punctului
GS<1> y =         % operatorii '='  şi '==' afişează valoarea asociată cheii indicate
7
GS<1> quit

În procedura ellipse redată mai sus, elipsa este descrisă pe baza operatorului predefinit arc; acesta are sintaxa "$x_c\;y_c\;r\;\alpha_1\;\alpha_2\;\mathrm{arc}$" şi are ca efect vizual producerea unui arc de cerc de rază $r$, cu centrul $(x_c, y_c)$, începând în sens antiorar de la $\alpha_1^{\circ}$ şi până la $\alpha_2^{\circ}$; pentru $\alpha_1=0$ şi $\alpha_2=360$ se obţine întregul cerc.
Precizăm totuşi că "efectul" real este crearea unei variabile de memorie (de tipul "path") conţinând coordonatele punctelor pe baza cărora se vor trasa curbele Bézier necesare; trasarea efectivă se declanşează prin invocarea ulterioară a operatorului stroke.

Dar cât de "mare" să fie cercul constituit astfel? Raza r este un număr (abstract, în matematică) şi n-avem decât să-l mărim dacă vrem un cerc "mai mare"; dar în tipografie, când avem de-a face cu o pagină de hârtie (sau cu o pagină de ecran), unitatea de măsură devine esenţială. În PostScript pagina de hârtie este asociată cadranului întâi al unui sistem de coordonate $xOy$ cu axa $Ox$ peste marginea de jos a paginii şi axa $Oy$ peste marginea stângă a acesteia; unitatea de măsură este $1\,\mathrm{bp}=\frac{1}{72}\,\mathrm{inch}$ (1 inch = 2.54 centimetri); arcele sunt măsurate în grade.

Sunt prevăzute transformări ale sistemului de coordonate iniţial, în două variante (dar numai aparent, distincte): putem utiliza matrici (de legătură între coordonatele vechi şi cele noi), având astfel şi un procedeu simplu pentru a salva şi a restaura sistemul de coordonate curent (salvăm "matricea de transformare curentă"); sau, putem folosi operatorii specializaţi translate (care mută originea în punctul indicat), rotate (care roteşte axele în jurul originii actuale, cu unghiul indicat) şi scale (prin care se poate stabili o nouă unitate de măsură, pe o axă, pe cealaltă, sau pe ambele axe de cordonate).

Operatorul arc produce un cerc (sau un arc de cerc) în sistemul de coordonate iniţial, dar dacă scalăm în prealabil cu $a$ pe orizontală şi cu $b$ pe verticală atunci, în sistemul de coordonate rezultat se va genera o elipsă cu semiaxele $a$ şi $b$.

În cazul nostru, a fost necesar să rotim elipsa în jurul centrului său, cu un anumit unghi şi a trebuit să ţinem seama de faptul că rotate roteşte axele în jurul originii sistemului de coordonate curent (nu în jurul unui alt punct) - ordonând transformările necesare astfel: întâi am translatat originea iniţială în punctul ale cărui coordonate le preluasem de pe stivă în xC şi yC; apoi am rotit în jurul noii origini cu unghiul $\mathrm{arctg}\,\sqrt{b}$; apoi am folosit 1 Sb scale, lăsând unitatea de măsură pe orizontală egală cu 1 pentru că semiaxa mare este 1 şi punând-o egală cu Sb (semiaxa mică) pe verticală; în final, am invocat 0 0 1 0 360 arc - obţinând "cerc" cu centrul (0,0) (adică originea sistemului de coordonate curent) şi de "rază" 1 (dar 1 pe verticală înseamnă acum valoarea din Sb, deci rezultă nu cerc, ci elipsă).

De încă un lucru a trebuit să mai ţinem seama: cu ce grosime urmează să trasăm elipsa obţinută? Grosimea implicită a liniei este de 1bp şi poate fi setată altfel prin operatorul setlinewidth; dar valoarea respectivă se raportează la scalarea curentă, astfel că în cazul nostru (când unitatea de măsură diferă între cele două axe) elipsa ar fi trasată (când se va invoca stroke) cu o "peniţă" de grosime variabilă. Pentru a evita aceasta, înainte de a efectua transformările arătate mai sus am salvat matricea transformării curente (desemnată prin CTM) şi am restaurat-o în final, după producerea elipsei; procedând astfel, scalările întreprinse în interior sunt uitate.
(grosimea implicită este de 1bp: într-adevăr, GS> currentlinewidth pstack ne dă 1.0)

Pentru a testa procedura ellipse, adăugăm în fişierul "grafic.eps" de exemplu:

72 72 scale  % noua unitate de măsură este de 72bp (= 1 inch = 2.54cm)
2 6 2 ellipse  % centrul elipsei este la 2in de marginea stângă şi 6in de baza paginii
.3 setlinewidth  % grosimea liniei este 0.3*72=21.6bp
.4 setgray  % nuanţa de negru pentru trasarea liniei
stroke  % trasează conturul (pe ecran, sau pe hârtie)

Transmiţând fişierul imprimantei - prin lpr grafic.eps - obţinem o pagină conţinând elipsa respectivă (precizarea "dacă imprimanta dispune de un interpretor propriu de PS" a devenit inutilă, în zilele noastre). Am prevăzut o grosime aşa de mare (21.6bp = 0.762 milimetri) pentru trasarea elipsei, în scopul de a testa cum arată elipsa dacă n-am mai salva/restaura CTM:

În stânga imaginii avem elipsa produsă prin programul de mai sus (cu salvare şi restaurare CTM), iar în dreapta aceeaşi elipsă (deplasată puţin spre dreapta, prin translate), produsă printr-o dublură a programului în care însă, am eliminat cele două linii pentru salvare/restaurare CTM. Desigur, aici am redat imaginea cu o anumită micşorare (pe hârtie, grosimea liniei pentru prima elipsă este de 7-8 milimetri), dar se vede suficient de bine că în cazul elipsei din dreapta grosimea liniei este variabilă (ajungând de două ori mai mare pe axa mare, decât pe axa mică).

vezi Cărţile mele (de programare)

docerpro | Prev | Next