Mini-cours au groupe de travail "Images"
MMI, Lyon
Séances du 29/01/2016 et du 18/03/2016.
Arnaud Chéritat
CNRS, IMT

Ces deux séances se sont divisées chacune en deux parties : notions de base sur les images informatiques, exemples de workflows ; conception de programmes pour dessiner des ensembles de Julia.

29 janvier 2016
1e partie
Il y a deux types d'images: Bitmap = en général une matrice rectangulaire de pixels carrés. À chaque point on donne une couleur.

Sur la majorité des images, les couleurs sont codées par des triplets RGB d'octets (entiers entre 0 et 28−1=255), ce qui donne un choix de 2563~16 million de couleurs différentes.

Voici une image Bitmap.

Si on zoome dessus, on voit les pixels:

Notez sur cet exemple les nuages de points épars. C'est du dithering. En effet, l'image du haut est au format GIF, un format très populaire au début d'Internet. Il permet des images fixes ou animées. Mais il est limité à 256 couleurs au total de sorte que la compression, LZW sans perte, reste efficace. On choisit ces 256 couleurs parmi les ~16 millions possibles à l'aide d'une palette codée en début de fichier, ce qui ne prend que 3×256=768 octets de plus. Le dithering permet d'interpoler entre ces couleurs par effet de moyenne (effet soit psychologique soit de flou en vision non centrale ou si les pixels sont assez petit). Aujourd'hui ce n'est plus nécessaire.

L'image ci-dessus se compresse bien car en plus elle contient de grands aplats. De taille 350×237 pixels, le GIF pèse 11 771 octets, alors que brute l'image pèserait 350×237 + 256×3 = 83 718 octets.


À propos des tailles de fichiers en informatique. J'adopte la convention qui semble être en vogue actuellement.

Il y a deux types de fichiers bitmaps :

La quasi-totalité des images sont stockées en brut en mémoire de travail et en compressé en mémoire de masse. Les formats bruts incluent BMP et TIFF (qui existent aussi en compressé)

Aujourd'hui une image brute en format full HD (1920×1080 pixels), couleurs standards (3 octets par pixels), prend environ 6Mo (précisément 6 220 800 octets).
Au milieu des années 1980, l'Atari ST proposait une résolution d'écran de 320×200 pixels en 16 couleurs. L'image brute pesait 32 000 octets, à comparer à la RAM de la machine, qui faisait 512Kio~524ko (pour le modèle initial).

Que ce soit sur le viel Atari ou sur une machine moderne, l'écran affiche le contenu de la mémoire video, qui contient une image RGB brute.* Quand aux logiciels qui manipulent les images bitmap, on imagine bien qu'ils doivent d'abord décompresser celle-ci pour pouvoir travailler sur les pixels. Notons qu'il se peut que les algorithmes de traitement raffinés utilisent des versions intermédiaires en plus haute densité de pixels ou en plus plus grande finesse de couleurs (2 octets par canaux R, G, B) avant d'effectuer une réduction en fin de traitement.

(*) Il y a eu des machines qui affichaient des images vectorielles, ce qui se prêtait bien aux écrans dits cathodiques (CRT) : un canon à électron envoyait un faisceau sur un écran électroluminescent. Ce faisceau était dirigé par un système électrique ou magnétique. Pour afficher une matrice de pixels, il fallait que le faisceau balaye tout l'écran plusieurs dizaines de fois par secondes tout en en modulant l'intensité. C'est ce que faisaient les télévisions CRT. Si on a le contrôle du faisceau, on peut à la place faire afficher des images vectorielles. C'est ce que faisait par exemple le jeu Asteroid, et que font encore certains oscilloscopes.

Il y a deux types de fichiers bitmaps compressés :

La compression sans perte permet de retrouver l'image originale. La compression avec perte est plus efficace au prix d'une différence que l'on espère presque invisible.
Les formats compressés les plus répandus sont PNG (sans perte) et JPEG (avec perte).

Comparons les tailles de fichiers avec un exemple:

Cette image est issue d'une photo haute qualité trouvée sur Wikipédia (CC-BY-SA, Photo by Laitche). Le bruit de l'image échantillonée par la caméra est assez faible. Cette dernière l'a convertie en JPEG mais avec un grand facteur de qualité (faible perte). L'auteur l'a déposée soit telle quelle, soit avec un post-traitement mais toujours en JPEG de grande qualité. Je l'ai récupérée, rognée et réduite (rééchantillonage par moyennes) ce qui a diminué le bruit.

La version ci-dessus fait 607×471 pixels. Brute elle pèse donc 858ko.

Sauvée en PNG, donc sans perte, elle pèse 608ko. Le gain est donc faible. En fait PNG est plus efficace sur des images constituées d'aplats.

L'image ci-dessous en 4200x4200 pixels (vous voyez une version réduite, l'original est là) en PNG mode palette fait seulement 500ko. Elle ferait ~18Mo en brut, avec palette.

Comparons maintenant avec JPEG:


JPEG, qualité maximale (100%), 393ko.


JPEG, qualité haute (78%), 53ko.

La différence est invisible à cette échelle. Zoomons:


PNG


JPEG 100%


JPEG 78%

On aperçoit sur la dernière les artefacts typiques du JPEG, échos fantômes près des frontières, découpage en blocs.


Une image vectorielle n'est pas décrite pixel par pixel : à la place, de grandes surfaces et courbes sont décrites par un faible nombre de paramètre, typiquement des points clefs : par exemple les coordonnées des sommets d'un polygone. Un des avantages, outre que c'est plus concis pour des images de type schéma, est que l'on peut changer l'échelle à loisir, l'image reste parfaite, alors qu'un bitmap sur lequel on zoome va faire apparaître les pixels.

Exemples de formats vectoriels, ou pouvant inclure du vectoriel, et répandus : SVG, PS (Postscript), PDF.

SVG (Scalable Vector Graphics)
Les navigateurs modernes savent afficher le SVG. Ci-dessous un exemple, créé avec Inkscape :

Le fichier fait seulement 1 564 octets, voici son contenu (pour info) :

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   version="1.1"
   id="svg2"
   viewBox="0 0 309.53674 179.60742"
   height="50.689205mm"
   width="87.358147mm">
  <defs
     id="defs4" />
  <metadata
     id="metadata7">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
        <dc:title></dc:title>
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <g
     transform="translate(-81.5468,-175.23288)"
     id="layer1">
    <path
       id="path4136"
       d="M 83.842661,212.92544 C 186.87822,90.696982 193.94929,326.06252 291.93409,219.99651 389.91888,113.93049 436.3859,352.32649 324.25897,353.33664 Z"
       style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffff00;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
  </g>
</svg>
C'est intéressant car c'est assez lisible, comparé par exemple à un fichier image qui est presque entièrement codé. Dans cet exemple, la forme en elle-même est très simple et est contenue dans la ligne en rouge d="M 83.842661,212.92544 C [...] Z". Son style est dans la ligne suivante, probablement plus longue que nécessaire. Le plus significatif étant l'épaisseur de ligne, le linejoin, et les couleurs de remplissage et de trait. Le reste contient des informations de transformation, les coordonnées de la fenêtre de vue, et des informations de version et métadonnées probablement superflues pour la plupart. Copie d'écran quand on ouvre le fichier avec le logiciel Inkscape (ici sous un Mac, mais il est disponible aussi pour Windows et Linux) :

La forme est ici définie par trois points de contrôle et 4 vecteurs (deux sont liés). Son pourtour est constitué d'un segment et de deux courbes de Bezier.

Pour comparer, j'ai demandé à Inkscape de sauvegarder ce fichier en PDF (1 056 octets), EPS=Postscript (2 572 octets), en PNG 1032×599 pixels (33 814 octets) et en PNG 344×200 pixels (8 661 octets).

Si vous êtes curieux, voici à quoi ressemblent les contenus des fichier PDF et (E)PS.

%PDF-1.5
%µí®û
3 0 obj
<< /Length 4 0 R
   /Filter /FlateDecode
>>
stream
[...
     ici des données en binaire, concises et illisibles
...]
endstream
endobj
4 0 obj
   157
endobj
2 0 obj
<<
   /ExtGState <<
      /a0 << /CA 1 /ca 1 >>
   >>
>>
endobj
5 0 obj
<< /Type /Page
   /Parent 1 0 R
   /MediaBox [ 0 0 247.629395 143.685928 ]
   /Contents 3 0 R
   /Group <<
      /Type /Group
      /S /Transparency
      /I true
      /CS /DeviceRGB
   >>
   /Resources 2 0 R
>>
endobj
1 0 obj
<< /Type /Pages
   /Kids [ 5 0 R ]
   /Count 1
>>
endobj
6 0 obj
<< /Creator (cairo 1.14.0 (http://cairographics.org))
   /Producer (cairo 1.14.0 (http://cairographics.org))
>>
endobj
7 0 obj
<< /Type /Catalog
   /Pages 1 0 R
>>
endobj
xref
0 8
0000000000 65535 f 
0000000571 00000 n 
0000000271 00000 n 
0000000015 00000 n 
0000000249 00000 n 
0000000343 00000 n 
0000000636 00000 n 
0000000763 00000 n 
trailer
<< /Size 8
   /Root 7 0 R
   /Info 6 0 R
>>
startxref
815
%%EOF
C'était le PDF. La forme est définie dans la partie binaire que je n'ai pas montrée. Voici le fichier EPS. (Le langage Postscript est très sommairement présenté un peu plus loin.)
%!PS-Adobe-3.0 EPSF-3.0
%%Creator: cairo 1.14.0 (http://cairographics.org)
%%CreationDate: Mon Jan 25 15:12:02 2016
%%Pages: 1
%%DocumentData: Clean7Bit
%%LanguageLevel: 2
%%BoundingBox: 0 -1 248 144
%%EndComments
%%BeginProlog
save
50 dict begin
/q { gsave } bind def
/Q { grestore } bind def
/cm { 6 array astore concat } bind def
/w { setlinewidth } bind def
/J { setlinecap } bind def
/j { setlinejoin } bind def
/M { setmiterlimit } bind def
/d { setdash } bind def
/m { moveto } bind def
/l { lineto } bind def
/c { curveto } bind def
/h { closepath } bind def
/re { exch dup neg 3 1 roll 5 3 roll moveto 0 rlineto
      0 exch rlineto 0 rlineto closepath } bind def
/S { stroke } bind def
/f { fill } bind def
/f* { eofill } bind def
/n { newpath } bind def
/W { clip } bind def
/W* { eoclip } bind def
/BT { } bind def
/ET { } bind def
/pdfmark where { pop globaldict /?pdfmark /exec load put }
    { globaldict begin /?pdfmark /pop load def /pdfmark
    /cleartomark load def end } ifelse
/BDC { mark 3 1 roll /BDC pdfmark } bind def
/EMC { mark /EMC pdfmark } bind def
/cairo_store_point { /cairo_point_y exch def /cairo_point_x exch def } def
/Tj { show currentpoint cairo_store_point } bind def
/TJ {
  {
    dup
    type /stringtype eq
    { show } { -0.001 mul 0 cairo_font_matrix dtransform rmoveto } ifelse
  } forall
  currentpoint cairo_store_point
} bind def
/cairo_selectfont { cairo_font_matrix aload pop pop pop 0 0 6 array astore
    cairo_font exch selectfont cairo_point_x cairo_point_y moveto } bind def
/Tf { pop /cairo_font exch def /cairo_font_matrix where
      { pop cairo_selectfont } if } bind def
/Td { matrix translate cairo_font_matrix matrix concatmatrix dup
      /cairo_font_matrix exch def dup 4 get exch 5 get cairo_store_point
      /cairo_font where { pop cairo_selectfont } if } bind def
/Tm { 2 copy 8 2 roll 6 array astore /cairo_font_matrix exch def
      cairo_store_point /cairo_font where { pop cairo_selectfont } if } bind def
/g { setgray } bind def
/rg { setrgbcolor } bind def
/d1 { setcachedevice } bind def
%%EndProlog
%%BeginSetup
%%EndSetup
%%Page: 1 1
%%BeginPageSetup
%%PageBoundingBox: 0 -1 248 144
%%EndPageSetup
q 0 -1 248 145 rectclip q
1 1 0 rg
1.836 113.534 m 84.266 211.315 89.922 23.022 168.309 107.873 c 246.699 
192.729 283.871 2.01 194.168 1.202 c h
1.836 113.534 m f
0 g
2.4 w
0 J
0 j
[] 0.0 d
4 M q 1 0 0 -1 0 143.685928 cm
1.836 30.152 m 84.266 -67.629 89.922 120.664 168.309 35.812 c 246.699 -49.043
 283.871 141.676 194.168 142.484 c h
1.836 30.152 m S Q
Q Q
showpage
%%Trailer
end restore
%%EOF
Dans les deux cas, les métadonnées indiquent qu'Inkscape a utilisé une bibliothèque appelée Cairo pour créer les fichiers. Pour le fichier (E)PS, on voit qu'ils définissent tout un tas d'alias et de fonctions, dont seul un petit nombre est utilisé.

Postscript (PS ou EPS=Encapsulated PostScript)
Postscript est non seulement un langage de description vectorielle d'image 2D, c'est également un langage de programmation. On peut par exemple faire calculer les décimales de π par un fichier Postscript. Si envoyez ce dernier à une imprimante Postscript, celle-ci calculera effectivement les décimales de π...

J'ai longtemps utilisé Postscript pour illustrer des articles de math. J'ai eu plusieurs méthodes de fabrication. C'est pour moi l'occasion de vous présenter quelques workflows.

Note : les navigateurs ne savent pas interpréter Postscript, les images ci-dessous sont donc traduites de PS vers SVG (j'ai utilisé Inkscape pour cela). Ce sont les fichiers PS dont je montre les sources.

Voici un exemple d'image que j'ai effectivement incluse dans un article en 2008 :

Créée avec Maple (Version 5). J'ai effectué le calcul précis de nombreux points sur deux courbes noires et une bleue, et demandé le dessin de ces courbe (Maple relie les points entre eux par des segments). Il y a aussi une zone en jaune délimitée par des arcs de cercles et une courbe. Et quelques autres formes.

J'ai conservé le fichier Maple servant à générer l'image mais je ne peux pas l'ouvrir pour l'instant. Si je trouve un moyen de le rendre lisible, je l'inclurai ici.

C'est une méthode que je n'utilise plus car Maple est payant et je change souvent de machine dans la journée. En général j'ouvrais le fichier PS en mode texte pour modifier certains paramètres qu'on ne pouvait pas modifier depuis Maple V, comme l'épaisseur de ligne.

Il est intéressant de jetter un oeil sur le contenu du fichier PS : voir ci-dessous. Sinon, le fichier est disponible pour téléchargement ici. Ce n'est pas requis pour la suite et le lecteur pressé peut sans problème sauter le code et ses explications ci-dessous.

Postscript utilise la notation post-fixe : les arguments sont placés avant les opérateurs, ainsi "350 212 moveto" va déplacer le crayon vers les coordonnées x=350 y=212.

On peut placer plus d'arguments que nécessaire : ils se retrouvent sur la pile, qui est du type last-in, first out. Par exemple "3 12 8 add mul 70 sub" fait la chose suivante : 3 12 et 8 se retrouvent sur la pile. "add" consomme les 2 derniers et met 20 sur la pile, qui contient maintenant 3 et 20. "mul" les consomme et met 60 sur la pile. On met 70 sur la pile qui devient 60, 70. Enfin sub les consomme et la pile ne contient maintenant plus que −10.

Le code "/m {moveto} def" permet de définir une nouvelle commande m qui va exécuter les opérations indiquées entre { et }. Ici "m" est donc devenu un alias pour "moveto".

Voici le code (absolument pas nécessaire pour la suite) :

%!PS-Adobe-3.0 EPSF
%%Title: Maple plot
%%Creator: MapleV 
%%Pages:  1
%%BoundingBox: 95 180 515 610
%%DocumentNeededResources: font Courier
%%EndComments
20 dict begin
gsave
/m {moveto} def
/l {lineto} def
/C {setrgbcolor} def
/Y /setcmykcolor where { %%ifelse Use built-in operator
       /setcmykcolor get
   }{ %%ifelse Emulate setcmykcolor with setrgbcolor
       { %%def
           1 sub 3 { %%repeat
               3 index add neg dup 0 lt { pop 0 } if 3 1 roll
           } repeat setrgbcolor
       } bind
   } ifelse def
/G {setgray} def
/S {stroke} def
/NP {newpath} def
%%%%This draws a filled polygon and avoids bugs/features
%%%%   of some postscript interpreters
%%%%GHOSTSCRIPT: has a bug in reversepath - removing
%%%%the call to reversepath is a sufficient work around
/P {gsave fill grestore reversepath stroke} def
%%%%This function is needed for drawing text
/stringbbox {gsave NP 0 0 m false charpath flattenpath
   pathbbox 4 2 roll pop pop 1.1 mul cvi exch 1.1 mul 
   cvi exch grestore} def
/thin 3 def
/medium 7 def
/thick 12 def
/boundarythick 20 def %% thickness of bounding box
%%IncludeResource: font Courier
36 36 translate
0.108 0.108 scale
1 setlinejoin
1 setlinecap
0.0 setgray
/inch {72 mul} def
/fheight 0.35 inch neg def
    0     0     0 C
thick setlinewidth
[] 0 setdash     1 0.94902     0 C
NP 1434 4933 m 1430 4930 l
1425 4927 l
1421 4924 l
1417 4921 l
1412 4918 l
1408 4915 l
1404 4912 l
1400 4909 l
1395 4906 l
1391 4903 l
[... 2800 autres lignes sur ce format ...]
1425 4927 l
fill
    1   0.6     0 C
NP 3334 5067 m 3332 5063 l
3330 5058 l
3327 5053 l
3325 5049 l
3323 5044 l
3320 5039 l
3318 5035 l
3316 5030 l
[... 1000 lignes ...]
3329 5068 l
3333 5065 l
fill
    0     0     0 C
NP 2463 3848 m 2469 3845 l
2472 3846 l
2476 3848 l
2479 3850 l
2482 3853 l
2485 3855 l
[... 540 lignes ...]
2498 3327 l
2500 3332 l
S
NP 2463 3848 m 2467 3846 l
2470 3847 l
2474 3849 l
[... 560 lignes ...]
2505 3332 l
2500 3332 l
S
    0     0     1 C
NP 2989 4067 m 2992 4064 l
2994 4061 l
2996 4058 l
2999 4055 l
3001 4052 l
3003 4049 l
[... 265 lignes ...]
2501 3322 l
2499 3325 l
2500 3332 l
S
NP 2989 4067 m 2500 3332 l
S
    1     0     0 C
NP
3048 3134 20 0 360 arc S
NP
2005 3030 20 0 360 arc S
NP
2463 3848 20 0 360 arc S
NP
2989 4067 20 0 360 arc S

showpage
grestore
end
%%Trailer
%%EOF

Voici un autre exemple, tiré du même article :

Pour le créer j'ai écrit directement en Postscript. Voici son contenu (donné à titre indicatif) :

%!PS-Adobe-3.0 EPSF
%%BoundingBox: 0 0 500 400 

1 setlinejoin
1 setlinecap

/L 100 def

/h 200 def

/h2 h 2 mul def

/b 10 def
/a 6 def
/c 40 def
/aa 12 def

/o1 -5 def
/o2 8 def
/o3 -30 def
/x1 -5 def
/x2 8 def
/x3 2 def

/d1 {/o o1 def /x x1 def } def
/d2 {/o o2 def /x x2 def } def
/d3 {/o o3 def /x x3 def } def

-10 200 translate

/draw {
0.95 setgray

/p0 {newpath 50 h moveto 0 h2 neg rlineto L 0 rlineto 0 h2 rlineto 
} def
/p1 {newpath 170 a sub h moveto 0 b h sub rlineto 10 a 2 mul add 0 rlineto 0 h b sub rlineto} def
/p2 {newpath 170 a sub h neg moveto 0 h b sub rlineto 10 a 2 mul add x add 0 rlineto 0 b h sub rlineto} def

%p0 fill
%p1 fill
%p2 fill

0.7 setgray
newpath 50 L add h moveto 0 L 2 div h b sub add neg rlineto a neg 0 rlineto 0 L 2 div h b sub add rlineto 
fill
newpath 50 h moveto 0 L 2 div h b sub sub rlineto a 0 rlineto 0 h b sub L 2 div sub rlineto 
fill

newpath 50 h neg moveto 0 L 2 div h b sub add rlineto a 0 rlineto 0 L 2 div h b sub add neg rlineto 
fill
newpath 50 L add h neg moveto 0 h b sub L 2 div sub o add rlineto a neg 0 rlineto 0 L 2 div h b sub sub o sub rlineto 
fill

newpath 170 h moveto a neg 0 rlineto 0 b h sub rlineto a 0 rlineto closepath  
fill
newpath 180 h moveto a 0 rlineto 0 b h sub rlineto a neg 0 rlineto closepath 
fill

newpath 170 h neg moveto a neg 0 rlineto 0 h b sub rlineto a 0 rlineto closepath  
fill
newpath 180 x add h neg moveto a 0 rlineto 0 h b sub rlineto a neg 0 rlineto closepath 
fill

0 setgray
2 setlinewidth

newpath 50 h moveto 0 h2 neg rlineto 
stroke
newpath 50 L add h moveto 0 h2 neg rlineto 
stroke
p1 stroke
p2 stroke

.4 setlinewidth

0 aa h {/i exch def newpath 50 i moveto L 0 rlineto stroke} for
aa neg aa neg h neg {/i exch def newpath 50 i moveto L 0 rlineto stroke} for
b aa h {/i exch def newpath 170 a sub i moveto 10 a 2 mul add 0 rlineto stroke} for
b neg aa neg h neg {/i exch def newpath 170 a sub i moveto 10 a 2 mul add x add 0 rlineto stroke} for
} def

/draw2 {
newpath 50 L add a 2 div sub b L 2 div sub c add moveto
170 a 2 div sub b c add lineto
stroke
newpath 180 a 2 div add b c add moveto
200 a 2 div add b L 2 div add c add lineto
stroke

newpath 50 L add a 2 div sub b L 2 div sub c sub o add moveto
170 a 2 div sub b c sub lineto
stroke
[13 20] 0 setdash
newpath 180 a 2 div add x add b c sub moveto
200 a 2 div add 50 L add sub b L 2 div add c sub lineto
stroke
[] 0 setdash

} def

gsave

d1
draw
150 0 translate
d2
draw
150 0 translate
d3
draw

grestore

2 setlinewidth

gsave

d1
draw2
150 0 translate
d2
draw2
150 0 translate
d3
draw2

grestore

newpath 180 a 2 div add 50 L add sub b c add moveto
200 a 2 div add 50 L add sub b L 2 div add c add lineto
stroke
Bon ça semble un peu effrayant mais c'est en fait relativement facile à écrire. Nettement plus facile qu'à lire d'ailleurs: c'est ce que les programmeurs appellent avec humour un write-only language. En plus je n'avais pas pris la peine d'inclure des commentaires. Il y a pas mal de copier-coller. J'y ai défini deux commandes "draw" et "draw2" que j'ai utilisées chacune 3 fois vers la fin.

Voici un troisième exemple, lié à un article de recherche de 2009. Pour celui-ci j'ai créé un programme qui a écrit le contenu du fichier PS.

Comme langage de programmation, j'ai choisi PHP (en CLI) pour sa facilité d'utilisation, la vitesse d'exécution n'étant pas critique.

Programme pseudo.php à appeler en ligne de commande "php pseudo.php n" avec n un entier (n vaut 7 sur l'exemple ci-dessus). Lisez si ça vous chante. (Là aussi il y a une absence totale de commentaire dans mon code. Ce qui peut aider, c'est quand le nom de certaines variables et fonctions sont descriptifs.)

<?php

if($argc!=2) {
  echo 'erreur : indiquer $links';
  exit(1);
}

$s="%!PS-Adobe EPSF-3.0
%%BoundingBox: 0 0 600 600
1 setlinecap
1 setlinejoin
/Courier findfont
14 scalefont
setfont
0.5 setlinewidth
";

$links=$argv[1];

$a=3;
$b=-1;
for($i=4; $i<$links; $i++)
{ $aux=$b;
  $b=2*$b+$a;
  $a=$aux;
}
$bn=2*$b+$a;
$nb=3*$b+$a;

$he=35;
$r0=75;
$K=25;

function sign($a) {
  return($a>0 ? 1 : ($a<0 ? -1 : 0));
}

function newpath() {
  global $new;
  $new=true;
  $s.="newpath\n";  
}

function to($x,$y) {
  global $he,$nb,$links,$r0;
  toto($y/$links,$r0+$he*($y-$x/$nb));
}

function toto($x,$y) {
  global $new,$s,$xs,$ys,$oldx,$oldy,$links,$K;
  if($new) {
    $u=$x*2*M_PI+M_PI/$links;
    $v=$y;
    $i=300+$v*cos($u);
    $j=300+$v*sin($u);
    $s.="$i $j moveto\n";
    $new=false;
  }
  else {
    for($k=0; $k<$K; $k++) {
      $u=($oldx+($x-$oldx)*$k/$K)*2*M_PI+M_PI/$links;
      $v=($oldy+($y-$oldy)*$k/$K)+M_PI/$links;
      $i=300+$v*cos($u);
      $j=300+$v*sin($u);
      $s.="$i $j lineto\n";
    }
  }
  $oldx=$x;
  $oldy=$y;
}


function recurse($i,$j) {
  global $x;
  $d=sign($j-$i);
  $e=abs($j-$i);
  if($e>2) {
    recurse($i,$j-$d);
    recurse($j-$d,$i+$d);
    recurse($i+$d,$j);
  }
  else {
    to($x,$i);
    to($x,$j);
    $x++;
  }
};

$s.="0 setgray\n"."newpath\n";
$new=true;
$x=0;
to($x,0);
$x++;
for($i=0;$i<$links;$i++) {
  recurse($i,$i+$links-3);
  recurse($i+$links-3,$i+1);
}
to($x,$links);
$s.="stroke\n";

$s.="showpage\n";
file_put_contents("pseudo.eps",$s);
Et voici à quoi ressemble le fichier Postscript généré :
%!PS-Adobe EPSF-3.0
%%BoundingBox: 0 0 600 600
1 setlinecap
1 setlinejoin
/Courier findfont
14 scalefont
setfont
0.5 setlinewidth
0 setgray
newpath
369.29096493835 328.70125742738 moveto
369.65377158236 328.85153685985 lineto
[...
     9623 lignes du même acabit 
                               ...]
368.36034023685 328.31578005454 lineto
stroke
showpage
Comme autres logiciels pour produire du Postscript, on peut citer le viel xfig, Inkscape qui peut sauver en ps ou pdf, cairo qui est une bibliothèque de programmation,

Pendant longtemps, le rendu des articles écrits en LaTeX se faisait en PS. Maintenant c'est du PDF. Les deux sont presque équivalents pour cet usage. On convertit l'un en l'autre assez facilement. Il se trouve en effet que PS et PDF sont deux formats crées par l'entreprise Adobe, le PDF étant une évolution de PS.

2e partie