Journal Générer des images vectorielles procédurales avec des technologies des années 2000

Posté par  . Licence CC By‑SA.
Étiquettes :
34
26
fév.
2024

Cher nal, récemment je ré-étudiais pour la n-ième fois le problème de concevoir des schémas simplement par un langage de description graphique (je n'aime pas les éditeurs visuels) avec potentiellement une partie générée procéduralement, pour faciliter certaines constructions. J'avoue que je suis plutôt du style « à l'ancienne », donc j'ai regardé le classique tikz (vraiment trop ésotérique quand on n'est pas un habitué du Latex), xfig (j'aime bien les vieilles interfaces, mais là bof), dia (que j'ai utilisé à ses débuts, mais ici trop spécifique aux diagrammes), mais aucun ne me convenait.

Comme le média de destination de ces schémas est le Web, j'ai essayé de regarder directement le format vectoriel standard pour celui-ci : Scalable Vector Graphics, ou SVG. Je l'ai souvent vu comme un format plutôt « bas-niveau », uniquement destiné à être le format final issu de sources plus « haut-niveau ». Mais j'ai eu une expérience il y a quelques années d'écriture directe de SVG qui retranscrivait une courbe de Bézier qui m'a bien plu, au point que je me suis dit que je devrais le regarder de plus près pour voir si ça ne serait pas possible d'écrire du SVG directement pour mon besoin.

Je me suis alors penché sur la très bonne documentation de Mozilla sur SVG et ça m'a permis de me mettre le pieds à l'étrier. J'ai d'abord trouvé un petit projet d'exemple de dessiner un minuteur, avec des formes pas trop compliquée : un cercle, quelques segments, etc. Et finalement, j'ai été supris par la simplicité d'écrire directement du SVG. Mais très vite j'ai eu besoin de générer une partie de ce SVG « procéduralement » (avec des constructions de répétition propres à un langage de programmation).

Comme vous le savez, SVG est basé sur XML, la technologie des années 20001 par excellence : un langage de balisage issu de HTML et SGML qui respecte un ensemble de règles bien définies, comme le fait d'être une structure arborescente. J'ai l'habitude de travailler sur du XML avec XSLT, un langage de transformation que je trouve super utile, même si sa syntaxe horripile tout le monde, moi y compris à certains moments. J'ai essayé d'imaginer comment faire ce que je voulais avec, mais l'orientation algorithmique du problème (générer 60 marques, dont une marque plus grosse toutes les cinq marques) commençait à me faire peur, vu les limitations du langage2.

J'ai alors pensé à une vieille connaissance que j'avais utilisé il y a deux décennies, à la grande époque de Python comme langage révolutionnaire pour le Web (au début des années 20000 ; ce qui a un peu foiré) : Zope. Pas pour tout le framework ORM qui l'accompagnait, mais pour son langage de templating (patronnage ça fait bizarre en français)TAL, qui respecte les principes de XML jusqu'au bout : le langage de template n'est pas invasif et respecte la structure du document (qui reste donc un fichier XML valide) contrairement à de nombreux moteurs de template modernes qui utilisent des syntaxes qui se superposent mal à XML ; il utilise avantageusement un namespace séparé pour ne pas rendre invalide le document ; il est basé sur Python, qui est le langage que je visais tout à fait pour l'écriture de la partie procédurale.

Seul problème : je veux TAL, mais je ne veux pas le reste de Zope. Et de toutes façons, Zope n'est plus disponible sur Debian depuis des lustres. Je regarde alors les alternatives, qui sont listées sur Wikipédia.

Je vois tout d'abord Simple TAL, qui semble répondre à mes besoins : pas de dépendance, simple, stable, etc. J'essaye, et ça marche plutôt bien : ça fait étrange de retrouver la syntaxe verbeuse de TAL, mais j'aime bien, même si la gestion d'erreur très rudimentaire fait que je me retrouve parfois avec des bouts de message d'erreurs mélangés à mon contenu… Bof bof, mais ça peut passer pour des petits travaux. Ça donne ça :

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200" height="200">

<g id="minutes" z="1">
<circle cx="100" cy="5" r="2"
    tal:repeat="angle python:[i*6 for i in range(60) if i%5]"
    tal:attributes="transform string:rotate(${angle}, 100, 100)"/>
</g>

<g id="fives" z="1">
<rect x="98.5" y="0" width="3" height="10"
    tal:repeat="angle python:[i*30 for i in range(12)]"
    tal:attributes="transform string:rotate(${angle}, 100, 100)"/>
</g>

<g id="slice" tal:define="timeangle python:int(path('time'))*6">
<!-- XXX fix z -->
<!-- XXX if angle is more than 180, 4th and 5th args to A have to change -->
<path 
    fill="red"
    z="0"
    tal:define="
        timesegx python:100-95*math.sin(path('timeangle')/360*2*math.pi);
        timesegy python:100-95*math.cos(path('timeangle')/360*2*math.pi)"
    tal:attributes="d string:M 100 5 L 100 100 L ${timesegx} ${timesegy} A 95 95 0 0 1 100 5 Z"/>

<rect x="98.5" y="5" width="3" height="95" />
<rect x="98.5" y="5" width="3" height="95"
    tal:define="angle python:-path('timeangle')"
    tal:attributes="transform string:rotate(${angle}, 100, 100)"/>
</g>

</svg>

Compte à rebours 10 minutes

Mais quelques semaines après, pour une seconde utilisation, je trouve cette approche pas encore assez à mon goût. Cette fois-ci, je veux reprendre un SVG existant, et le « génériciser ». Ce fichier c'est une représentation des flottants en informatique https://en.wikipedia.org/wiki/File:IEEE_754_Double_Floating_Point_Format.svg

Je reprends la liste des alternatives ZPT, et je regarde de plus près Chameleon (disponible sur Debian avec le package python3-chameleon) : le projet semble lié à Pyramid, un framework Web que j'aime beaucoup et que j'ai utilisé il y a dix ans, basé sur des idées vraiment bonnes selon moi (allez voir ce que font les mecs du projet Pylons pour avoir une meilleure idée). Par exemple, il parse la template en bytecode Python directement, pour éviter le jonglage avec des traductions intermédiaires de source, ça me semble bien. Il est fourni uniquement sous forme de bibliothèque, pour faciliter son intégration3.

Du coup, après avoir passé au départ un gros temps de nettoyage du SVG généré par Inkscape, je commence à tester, et ça marche vraiment bien : assez vite, j'utilise le fait que le type d'expression par défaut est Python, et non les chemins d'objet ZPT (qui sont bien adaptés pour l'ORM de Zope, mais moins quand on a un code Python simple à 100%), ce qui simplifie beaucoup le code. Puis je remplace les très XML-èsque tal:attributes (qui me rappellent les xsl:attribute) par l'utilisation de l'insertion de texte directe sur des attributs, qui sont donc littéralement écrits avec comme valeur une expression utilisant la syntaxe ${…}, issue de Genshi. Elle remplacera même plus tard même les tal:replace ou tal:content pour les contenus d'élément texte. Certes, on se rapproche ainsi plus des moteurs de template classiques, mais ça ne casse au moins pas la structure, qui serait plus amochée si je n'utilisais pas les tal:repeat, ou la définition de variables utiles avec tal:define.

J'ai au début uniquement programmé la répétition des barres de l'image, puis des points sous la barre. Puis je suis allé encore plus loin pour ce cas précis de flottant en informatique, en généricisant pour n'importe quelle taille d'exposant ou de fraction. Enfin j'ai rendu dynamique le positionnement du texte au-dessus, ce qui me permet au passage de le passer en français. Et au final, ça donne ça :

<?xml version="1.0" encoding="UTF-8"?>
<!-- 
  This work is adapted from "IEEE 754 Double Floating Point Format"
  <https://commons.wikimedia.org/wiki/File:IEEE_754_Double_Floating_Point_Format.svg>
  by Codekaizen <https://commons.wikimedia.org/wiki/User:Codekaizen>,
  used under CC BY SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0/>.
  This work is licenced under CC BY SA 4.0 <https://creativecommons.org/licenses/by-sa/4.0/>
  by Benjamin Cama <benoar@dolka.fr>.

  Date: 2024-02
-->
<!--!
  Parameters:
    exponent: The float’s exponent size, in bits.
    fraction: The float’s fraction size, in bits.
-->
<?python
def nbars(n):
    "Size of `n` bars."
    return n * 9
?>
<svg xmlns="http://www.w3.org/2000/svg"
    xmlns:tal="http://xml.zope.org/namespaces/tal"
    tal:define="exponent int(exponent); fraction int(fraction); start 28"
    width="${ start + nbars(1 + exponent + fraction + 1) }" height="100">
  <defs>
    <rect id="bar" width="${nbars(1)}" height="28"/>
  </defs>
  <style>
    #bar    { stroke: black; }
    .above  { stroke: black; fill: none; }
    text    { font-size: 12px; text-align: center; text-anchor: middle; font-family: sans }
    circle  { stroke: black; fill: black; fill-opacity: 0.25; }
  </style>
  <g>
    <!-- bars -->
    <use href="#bar" tal:repeat="i range(1)"
        style="fill:#d5ffff" x="${ start + nbars(i) }" y="44"/>
    <use href="#bar" tal:repeat="i range(1, 1+exponent)"
        style="fill:#a4ffb4" x="${ start + nbars(i) }" y="44"/>
    <use href="#bar" tal:repeat="i range(1+exponent, 1+exponent+fraction)"
        style="fill:#ffb2b4" x="${ start + nbars(i) }" y="44"/>

    <!-- sign -->
    <g tal:define="x start + nbars(0 + 1/2)">
      <text style="text-anchor: end">
    <tspan x="${ x + 2 }" y="25" >signe</tspan>
      </text>
      <path class="above" d="M ${x},31.5 L ${x},41.5"/>
    </g>

    <!-- exponent -->
    <text tal:define="x start + nbars(1 + exponent/2)">
      <tspan x="${x}" y="12.5" >exposant</tspan>
      <tspan x="${x}" y="25" >(${exponent} bits)</tspan>
    </text>
    <path tal:define="x1 start + nbars(1); x2 start + nbars(1 + exponent)"
        class="above" d="M ${x2-1},41.5 L ${x2-1},31.5 L ${x1},31.5 L ${x1},41.5"/>

    <!-- fraction -->
    <text tal:define="x start + nbars(1 + exponent + fraction/2)">
      <tspan x="${x}" y="12.5">fraction</tspan>
      <tspan x="${x}" y="25" >(${fraction} bits)</tspan>
    </text>
    <path tal:define="x1 start + nbars(1 + exponent); x2 start + nbars(1 + exponent + fraction)"
        class="above" d="M ${x2},41.5 L ${x2},31.5 L ${x1+1},31.5 L ${x1+1},41.5"/>

    <!-- bit dots -->
    <g tal:repeat="b (0, fraction, fraction+exponent)">
      <g tal:omit-tag="" tal:define="x start + nbars(1/2+exponent+fraction - b)">
    <circle cx="${x}" cy="79" r="3.7"/>
    <text x="${x}" y="93">${b}</text>
      </g>
    </g>
  </g>
</svg>

Et je suis vraiment content du résultat. C'est plutôt propre, relativement conforme à ce qu'on attend d'un XML, au point où j'imagine même une chaîne de traitement qui pourrait combiner TAL et XSLT ! Ah, et pour voir le résultat, sur un double par exemple :

Flottant à double précision

Le seul bémol par rapport à la « standardicité » de ce template utilisant TAL, c'est que Chameleon ré-utilise l'espace de nommage (namespace) original de Zope alors qu'ils ont en fait étendu le dialecte pour l'insertion directe par exemple, ou le fait que les expressions soient en Python par défaut (il y a d'autres nouveautés également). Dans un soucis de compatibilité ascendante, idéalement, ils auraient dû définir un nouveau namespace pour ces extensions4.

Mais justement, rien que le fait d'utiliser un namespace pour intégrer de la « procéduralité » dans un SVG va permettre de montrer une possibilité géniale de XML : de pouvoir être une structure de données mélangeant divers aspects de manière non-intrusive (ou orthogonale), ce qui permet par exemple dans notre cas de modifier le SVG avec Inkscape par exemple, et de continuer à obtenir un template valide ! Ça fonctionne moyennement avec le template ci-dessus, car le dessin ne ressemble à rien vu que par exemple les coordonnées des objets n'ont pas de valeur correcte : on utilise l'insertion directe avec des ${x} par exemple, qui n'est pas une coordonnée valide. C'est pour ce cas où rester avec la syntaxe strictement XML d'origine de TAL peut être utile : ces attributs auront une valeur qui est utile pour présenter ce dessin dans un logiciel d'édition de SVG comme Inkscape, mais seront remplacés lors de l'instanciation du template par TAL ! Essayez avec cette version du dessin, vous verrez que le template continue d'être utilisable et applique les modifications que vous y avez effectué.

Et ça c'est la puissance de XML qui n'a malheureusement jamais été beaucoup développée : pouvoir intégrer plusieurs dialectes ou formats (avec son propre namespace) au sein d'un même fichier pour qu'il soit « multiforme ». J'avais il y a quelques années ainsi intégré une extension à un diagramme à état en SCXML qui me permettait de travailler sur un aspect orthogonal au diagramme, en utilisant au passage un namespace que j'avais créé pour l'occasion en utilisant les Tag URI. Vous voyez d'ailleurs qu'Inkscape intègre lui-même ses propres données au fichier SVG standard à travers le namespace souvent préfixé « sodipodi » (le nom d'origine du projet) pour y stocker ses paramètres. Dans un autre contexte, RDFa est une manière d'étendre le HTML pour faire du RDF, en trouvant un entre-deux n'utilisant que des attributs préfixés et non pas des éléments spécifiques, afin de concilier le divorce du HTML moderne avec XML (mais faites du XHTML5 même s'il n'a pas de schéma !).

Bref, j'espère que ce journal t'auras un peu réconcilié avec XML, et puis donné envie de lier Python et XML pour ton prochain projet. Tu trouveras tous les fichiers de mon expérimentation ici : http://dolka.fr/bazar/tal-svg-experiment/


  1. j'ai toujours trouvé ça étrange mais quand on dit « les années XXXX », on parle de la dizaine d'année qui commence par l'année citée ; dans notre cas, les années 2000--2010. 

  2. en fait, XSLT n'est vraiment pas fait pour de la génération procédurale, mais pour du traitement de données en flux. Il est idéal pour transformer un document en un autre, mais laissez tomber dès que la part de calcul devient trop importante. À la limite, vous pouvez partir d'un programme dans un langage classique pour la partie calcul, et sortir un document « de base » –  en utilisant même des printf() à la barbare avec une structure simple  – à transformer plus aval par XSLT. Mais n'essayez pas de tout faire en XSLT. 

  3. du coup Chameleon étant une bibliothèque seulement, j'ai créé un petit outil talproc pour traiter les templates TAL. Il s'utilise à la manière de xsltproc avec les paramètres du template passés en --stringparam. Par exemple, pour générer le fichier du flottant double précision : talproc --stringparam exponent 11 --stringparam fraction 52 IEEE_754_Floating_Point_Format-direct-template.svg 

  4. pour troller, je voudrais signaler que Microsoft, pourtant habitué au EEE, a correctement namespacé ses extensions à OOXML et continue de le faire des années après, cf. https://learn.microsoft.com/en-us/openspecs/office_standards/ms-docx/d0a2e301-0ff7-4e9e-9bb7-ff47070dce0a issu de https://learn.microsoft.com/en-us/openspecs/office_standards/ms-docx/b839fe1f-e1ca-4fa6-8c26-5954d0abbccd. Après, vous pourrez arguer qu'avoir un « standard » qui continue d'avoir des extensions (à l'utilité discutable, si vous regardez le contenu précisément) tous les deux ans, ça n'est pas vraiment une forme de stabilité qui lui confèrerait un caractère si « standard », mais bon… 

  • # Défis

    Posté par  . Évalué à 2 (+1/-1).

    Et pour ceux qui aiment les défis, vous pouvez par exemple essayer de faire la même chose avec ce joli exemple en tikz :
    https://fr.wikipedia.org/w/index.php?title=Fichier:Neighbourhood_definition2.svg&lang=fr

    Et pour ceux qui se sentent encore plus en forme, je vous laisse réfléchir à une solution multi-dialecte en JSON… Non, pardon, je déconne, c’est pas possible ! (ceux qui me sortent du JSON-LD – fils batard de JSON et RDF – je leur lance des œufs pourris)

    • [^] # Re: Défis

      Posté par  (site web personnel) . Évalué à 3 (+1/-1).

      As-tu essayer d'utiliser les téchniques comme elle de Reactjs ? Il mélange une structure HTML (et donc XML) avec le javascript de façon assez propre. Cela permettrait cacher les détails d'implémentation que l'on voit, mais on perd la compatibilité SVG.

      "La première sécurité est la liberté"

      • [^] # Re: Défis

        Posté par  (site web personnel) . Évalué à 3 (+1/-1).

        En fait cela se fait out of the box avec reactjs :

        https://www.smashingmagazine.com/2015/12/generating-svg-with-react/

        "La première sécurité est la liberté"

        • [^] # Re: Défis

          Posté par  . Évalué à 3 (+1/-0).

          Je n’ai jamais utilisé ReactJS mais j’avais déjà vu cette syntaxe, d’abord avec E4X, puis effectivement celle-ci issue de React qui s’appelle JSX. Je la trouve vraiment pratique, et c’est vraiment bien pour facilement intégrer du *ML, mais ça reste un fichier de syntaxe principale Javascript, avec des bouts de XML dedans, et non du XML « pur ». Je suis d’accord que c’est l’avantage et l’inconvénient de XML : c’est puissant, mais du coup la syntaxe est naze pour du code, d’où le rejet de XSLT par plein de monde.

          Le côté « descriptif » du XML permet de rester sur de la donnée arborescente (à la Lisp en fait ; je pensais que quelqu’un allait me le sortir au moins…) qui est interprétée comme du code, et a donc cette facilité d’être abordée de multiples angles. Un langage de programmation peut bien sûr être vu comme un arbre (le T de AST) mais son côté plus proche du langage naturel, qui est son avantage, gêne les autres utilisations, je trouve. J’ai toujours voulu trouver des notations de XML plus proches d’une syntaxe sans balise, et il y a quelques essais (je me souviens de YML je crois — non, pas YAML — mais qui est introuvable aujourd’hui dans un moteur de recherche…) mais aucun qui ne m’a vraiment convaincu.

      • [^] # Re: Défis

        Posté par  . Évalué à 4 (+2/-0).

        J'ai du mal à voir ça propre : du HTML/XML qui apparaît en plein milieu de code JS, ça m'a toujours fait pensé à du spaghetti.

        Je suis beaucoup plus à l'aise sur le choix de Vue.js.

        Mais bon, ici, c'est pas le sujet, c'est plutôt d'utiliser le XML et ses espaces de noms pour le triturer avec deux logiciels différents. C'est beau.

        • [^] # Re: Défis

          Posté par  (site web personnel) . Évalué à 3 (+0/-0).

          Il me semblait que vue.js et reactjs fonctionne de la même façon.

          Pour avoir à travailler avec des template, où tu finis par injecter des strings dans des variables pour faire ta variabilité, reactjs semble 1000 fois plus propre (et les hooks sont énormes, mais c'est un autre sujet).

          En fait, ton arborescence est native comme peut l'être une string dans un code plus classique.

          "La première sécurité est la liberté"

          • [^] # Re: Défis

            Posté par  . Évalué à 2 (+0/-0).

            Il me semblait que vue.js et reactjs fonctionne de la même façon.

            JSX n'est pas utilisé par vue. Ce dernier met tout dans le même fichier, mais de manière un peu plus séparé :

            <script setup>
            import { ref } from 'vue'
            
            // A "ref" is a reactive data source that stores a value.
            // Technically, we don't need to wrap the string with ref()
            // in order to display it, but we will see in the next
            // example why it is needed if we ever intend to change
            // the value.
            const message = ref('Hello World!')
            </script>
            
            <template>
              <h1>{{ message }}</h1>
            </template>

            La façon jsx de mélanger la vue et le code se rapproche probablement plus d'elm :

            import Html exposing (..)
            
            
            main =
              div []
                [ h1 [] [ text "My Grocery List" ]
                , ul []
                    [ li [] [ text "Black Beans" ]
                    , li [] [ text "Limes" ]
                    , li [] [ text "Chili Powder" ]
                    , li [] [ text "Quinoa" ]
                    ]
                ]

            https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

  • # Juste pour être exhaustif

    Posté par  . Évalué à 4 (+2/-0).

    …car pas sûr que ça convienne mais tu n'as pas cité Mermaid pour générer des SVGs:
    https://github.com/mermaid-js/mermaid
    http://mermaid.js.org/#/

    Qui doit pouvoir être utilisé en ligne de commande:
    https://github.com/mermaid-js/mermaid-cli

  • # proutes

    Posté par  . Évalué à 2 (+1/-0).

    Bref, j'espère que ce journal t'auras un peu réconcilié avec XML

    En plus de ça il m'a donné envie d'essayer Pyramid, et de faire des proutes.

  • # Coquille

    Posté par  (site web personnel) . Évalué à 2 (+0/-0).

    « […] « les années XXXX », […] la dizaine d'année qui commence par l'année citée […] »

    Ne serait-il pas plus approprié d'écrire XXX0 à la place de XXXX ? Partant, la pratique paraîtrait-elle moins incongrue ? Un peu comme en science expérimentale où la représentation numérique s'arrête dans la zone où débutent les incertitudes ?

    « IRAFURORBREVISESTANIMUMREGEQUINISIPARETIMPERAT » — Odes — Horace

    • [^] # Re: Coquille

      Posté par  . Évalué à 2 (+0/-0).

      On termine effectivement toujours les années utilisées dans cette expression par 0 ; je ne comprends pas trop ta remarque. C’est effectivement pour rappeler la notation scientifique, je crois.

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.