Générer des certificats d’authenticité d’œuvres d’art avec LaTeX et GPG

 x0r   0
photo gpg cryptographie art vente

J’ai récemment eu l’occasion de vendre une de mes photos, dont j’avais fait tirer une petite série de cinq exemplaires numérotés et signés dans un laboratoire spécialisé en tirages d’art. Je ne l’avais pas vraiment prévu, car ça s’était passé au détour d’une conversation.

Cette vente était une petite source d’angoisse, car quand on vend une photo, il convient de fournir à l’acheteur un certificat d’authenticité, prouvant l’origine (et l’originalité) de l’œuvre achetée. Ce que je n’avais jamais fait avant.

Ces certificats d’authenticité sont généralement édités sur des papiers spéciaux, avec des encres spéciales, accompagnés de filigranes, hologrammes et autres mesures de sécurité pour en dissuader la falsification. Mais je n’avais pas du tout le temps pour faire éditer un certificat sous cette forme-là ; je n’avais qu’une soirée pour fournir à mon acheteur quelque chose de semblable. Heureusement, puisqu’il a des notions solides en cybersécurité, je savais que je pouvais improviser quelque chose avec rien d’autre que LaTeX, GPG, qrencode et ImageMagick.

Mais alors, comment créer ce certificat ?

En guise d’exemple, je vais utiliser la photo ci-dessous, que j’ai prise récemment :

Photo de TGV Duplex
TGV 2N2-3UF (rame 820), assurant le train 5316 (Le Havre – Marseille), photographiée le 15 février 2022.

Imaginons que j’aie fait tirer cette photo à dix exemplaires par mon laboratoire de prédilection, puis que j’aie vendu l’exemplaire numéro 1 de cette série. Je vais donc préparer un certificat d’authenticité qui reprend les informations ci-dessous, que je sauvegarde par ailleurs dans un fichier texte (appelons-le meta.txt) :

Auteur: x0r
Titre: TGV 2N2-3UF (rame 820)
Date de la prise de vue: 15 février 2022
Numéro du tirage: 1/10
Format: 45 × 30 cm
Type d'impression: Piezography - Encres pigmentaires Epson UltraChrome™ HDR
Papier: Platine Fibre Rag 310 g/m² Canson

C’est une bonne idée d’ajouter, sur le certificat, une petite vignette de l’image en question. Je la prépare donc avec une résolution suffisante pour apparaître à environ 300 dpi sur la feuille :

% convert photo -quality 80 -resize 1000x thumb-photo.jpg

Avec tout ça, on a de quoi faire un certificat d’authenticité avec LaTeX ; je pourrais alors l’imprimer, la signer et m’arrêter là. Mais n’importe qui pourrait le falsifier, alors que faire ?

C’est là où GPG peut nous aider. Tout d’abord, je signe mon fichier meta.txt avec GPG :

% gpg --clear-sign meta.txt

J’utilise l’option --clear-sign afin de conserver le contenu de meta.txt lisible. Le fichier signé, meta.txt.asc, ressemble à ceci :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

Auteur: x0r
Titre: TGV 2N2-3UF (rame 820)
Date de la prise de vue: 15 février 2022
Numéro du tirage: 1/10
Format: 45 × 30 cm
Type d'impression: Piezography - Encres pigmentaires Epson UltraChrome™ HDR
Papier: Platine Fibre Rag 310 g/m² Canson
-----BEGIN PGP SIGNATURE-----

iQIzBAEBCgAdFiEE/JoAQPj3zjdYk5ClL0gWKnq2LYYFAmJ2ZvQACgkQL0gWKnq2
LYZ0RBAAwYLAjfHcz26d9FTJemxWGattypV6m7BReH9n9XDUSP8TR0n0fjgw5sf5
meoddDc5SyXZW8O9wZVYZygnFiZIo98TQCCkWfWgLc2wOPMNKZAT7s1c6kgcFIrU
wldKq0udGtF5psRYWd6pLjgjNFMzB8lk+SPM6zi8pS/9pJcNqrYTCTbB+Pdbbrm2
NJcLwIn6gCRNUtz51dLtWywszej9nmY1E00QqTzBaFzxisGX1S64jiOawprYcNHr
ysrUzKvWTg8ywC9h9g47lP5ZkN++ak87y9iqkojwfky5aVdsfC/S14wB5vADmUEg
9jIbVbG2YWaA9bFPvgw0vCPm/qLGhmQ9ARTHMrLJj6uBkoQQTh98EBvp0oyA38c0
qJNZSt6W5bD/MJyZsZ9HY/yRzpswNa0KLzYH5dFQ2QhmkhnFHJ6VwZTGMNefZFDn
qz058EAWDM3rq7beLz0ksAjImOn8s4dr3k7kA0ydUrqRANZsSl0cUlwKKn5j412v
YuvjCud9oBFtczFbz4K21VKrDlTPRL2q2rQpsX1UZyLezm4MfJTmYd1IEEJrkC/F
6t5xbnlcomAshQK9+3XI96Jog4CMTcSGZwux1VasbsCWmONh9aE6CRmxB3CZuD6x
NMNGcSHVnhH56hjlP/RYv5X9IzRoChh3e4+nvDQVYLRNweoRldk=
=yE6O
-----END PGP SIGNATURE-----

Puis, on en fait un code QR.

% qrencode -r meta.txt.asc -o meta_qr.png

L’ajout de la signature cryptographique multiplie par plus de quatre la taille de l’entrée : on passe de 249 à 1 131 octets. Mais on reste largement sous la limite permise par la spécification, car le résultat est un code QR version 24, de 113 × 113 pixels, alors que les codes QR les plus grands, appelés « version 40 », ont pour dimensions 177 × 177, soit presque 2,5 fois la surface d’une version 24.

Comme j’utilise XeLaTeX, il faut d’abord convertir cette image au format PDF :

% convert meta_qr.png meta_qr.pdf

Et ensuite, on peut l’embarquer dans le document. J’y ajoute un paragraphe décrivant l’utilité de cet énorme QR code, ainsi que l’empreinte de la clef publique ayant permis de signer les métadonnées.

Certificat d’authenticité avec un QR code

Et voilà le résultat : un joli document avec toutes les informations à propos de ma photo vendue, avec une signature électronique qui protège contre la majorité des fraudes. Une fois imprimée, j’y ajoute juste une signature de ma propre main, en utilisant une encre indélébile.

En pratique, le code QR a besoin de faire au moins 6,4 cm de côté pour qu’il puisse être lu par un téléphone. Avec une imprimante à jet d’encre, les points du QR code peuvent donc avoir tendance à « baver » ; descendre en-dessous de cette taille risque de rendre ce code QR inexploitable.

Vérification de la signature

Une signature électronique n’aurait que peu d’intérêt s’il n’y avait aucune façon de la vérifier.

Avec un téléphone Android, c’est facile : il faut d’abord installer une application de scan de codes QR (j’utilise QR Scanner de SECUSO Research Group, mais si vous en connaissez d’autres, sans publicité, qui sont recommandables, je suis tout ouïe), puis OpenKeychain pour gérer la partie GPG.

Puis il faut importer ma clef publique dans l’application OpenKeychain (il n’est pas forcément nécessaire de créer des clefs GPG).

Ensuite, pour vérifier ma signature numérique, il faut procéder ainsi : scannez le QR code, sélectionnez l’icône « Partager », puis l’icône OpenKeyChain avec la légende « Déchiffrer… ». Vous devriez alors voir à l’écran l’indication que la signature est bonne.

Après avoir scanné le code QR, on peut vérifier que la signature est bonne.

Que se passe-t-il si j’essaye de falsifier le contenu du QR Code ? Ici, j’ai changé le numéro du tirage de 1/10 à 1/30 dans meta.txt.asc, puis j’ai généré un nouveau code QR. Lorsque je le scanne, le changement est visible dans le texte en clair… mais OpenKeychain m’affiche un gros avertissement, indiquant que quelqu’un a modifié le texte.

Mais si je modifie ne serait-ce qu’un caractère, la signature n’est plus valable.

Conclusion

Vous savez maintenant comment j’ai improvisé un certificat d’authenticité pour des tirages d’art que je ne pensais pas forcément vendre de façon organisée.

Bien entendu, si j’avais vraiment voulu vendre mes photos de façon habituelle (autrement dit, si j’avais pris le statut d’artiste-photographe par exemple), je me serais tourné vers des certificats d’authenticité édité par des services visant un public de photographes professionnels. Pour les acheteurs, vérifier un document comme celui-ci nécessite tout de même d’être familiarisé avec GPG, une compétence que je ne pourrais pas exiger d’acheteurs « lambda ».

Au moins, la seule façon réaliste de frauder serait de générer un document similaire, mais de tout signer avec une clef qui ne serait pas la mienne. Seule une personne avertie qui connaîtrait préalablement l’empreinte de ma vraie clef publique pourrait alors déceler le subterfuge. Mais cette méthode me donne, pour mon cas d’utilisation, un niveau de protection qui me semble tout à fait acceptable.

En fabriquant ces certificats d’authenticité, j’ai beaucoup pensé aux non-fungible tokens (NFT), la nouvelle mode du moment (et bien expliqués dans cet article). Je ne me suis pas intéressé de plus près à ce nouvel objet de spéculation, mais pourtant, la question se pose : aurais-je créé une sorte de « NFT physique » sans le savoir ?

Le contenu du fichier meta.txt.asc tout seul ne suffirait pas pour qualifier de NFT : j’y ai par exemple omis une empreinte (SHA-512 par exemple) du fichier JPEG que j’ai fait tirer, car la miniature figurant sur le certificat joue déjà ce rôle. Ce n’est donc pas meta.txt.asc qui jouerait le rôle de « NFT physique », mais bel et bien le document imprimé complet. Même si là encore, ça se discute : cette feuille de papier ne rentrera dans aucune chaîne de bloc et je ne peux pas non plus y attacher un contrat automatique stipulant que je touche de l’argent à chaque revente. Mais là, comme ça, c’est déjà pas mal.

Liens externes

Trois applications d’Asterisk à la maison

 x0r   0
sip asterisk

Je dois faire partie d’une infime minorité de gens qui, à mon âge, utilise le téléphone fixe et je dois bien faire partie d’une minorité encore plus infime en utilisant cette ligne fixe avec un système Asterisk à la maison.

Ce que j’avais à l’origine installé chez moi pour jouer s’est révélé être de plus en plus utile avec le temps. D’abord parce que feu mon projet siproxd_orange a été une excellente façon d’apprendre le SIP. Mais ensuite parce que mon infrastructure existante m’a permis de développer de petites applications pas forcément très compliquées, mais très utiles. Autrement dit, mon serveur Asterisk est passé du statut de solution en quête de problèmes à une solution pour de vrais besoins que j’ai fini par avoir plus tard.

J’ai pu en effet mettre en œuvre récemment quelques idées d’applications utiles et, de surcroît, appréciées par ma bien-aimée. Ces applications ont pour point commun d’exploiter une fonction que proposent certains téléphones SIP : le décroché automatique sur haut-parleur pour certains appels entrants. Dans ce billet, je vous en propose trois : un simple système d’interphone, un système de diffusion globales d’annonces vocales et un système de rappels automatique par diffusion globale.

Avant de commencer, je tiens à préciser que j’utilise PJSIP en faveur de l’ancien module chan_sip que je ne compile même plus dans mes propres installations d’Asterisk et qui est officiellement déprécié depuis Asterisk 19. Si vous utilisez encore chan_sip, à vous d’adapter mes exemples.

Interphone

Imaginons le scénario suivant : je viens de finir ma cuisine pendant que Madame est sur son PC en train de jouer. Pour l’appeler à table, je pourrais crier à travers l’appartement « À table ! », mais je n’ai pas envie de m’égosiller à chaque fois. Je pourrais aussi composer, depuis le téléphone de la cuisine, le numéro du poste à son bureau : mais alors, comme elle a les deux mains occupées, elle ne peut pas décrocher.

La solution serait ce que j’appellerai ici « interphone » par souci de concision : c’est-à-dire la possibilité pour moi d’appeler un poste et que cet appel soit automatiquement décroché sur haut-parleur.

Certains modèles de téléphones SIP déclenchent une prise automatique d’appel sur haut-parleur si un en-tête particulier se trouve dans le message INVITE, en général Alert-Info, et a une valeur particulière. En tout cas :

  • sur des Yealink W52P, il faut un Alert-Info égal à Auto answer ;
  • sur des Polycom SoundPoint IP 450, la valeur nécessaire est configurable, alors j’ai fait en sorte de s’aligner sur les Yealink.

Pour d’autres constructeurs, il y a peut-être besoin d’une valeur différente, d’un en-tête différente ou alors la fonction n’existe pas du tout.

Pour ajouter cet en-tête dans le message INVITE envoyé à l’appelé, il faut pouvoir positionner la variable PJSIP_HEADER(add,Alert-Info) du demi-appel (call leg) entre Asterisk et l’appelé. Le faire directement dans l’extension (i.e. faire un Set juste avant le Dial vers le destinataire) ne marchera pas, car cela agit sur la signalisation SIP entre l’appelant et Asterisk.

Il faut donc passer par une astuce de l’application Dial : l’option b permet d’indiquer vers quelle extension effectuer un Gosub dans le contexte du demi-appel entre Asterisk et l’appelé.

J’ai créé un contexte handlers, dans lequel je place des extensions qui ne permettent pas d’appeler des téléphones. L’extension dont j’ai besoin, que j’ai appelée set_auto_answer, est définie ainsi :

[handlers]

exten => set_auto_answer,1,NoOp()
 same => n,Set(PJSIP_HEADER(add,Alert-Info)=Auto Answer)
 same => n,Return()

Enfin, j’ai choisi de mettre en œuvre l’accès à ce service en préfixant le numéro du poste par *. Par exemple, 4042 déclenche un appel classique mais *4042 donne un appel d’interphone. Il ne reste plus qu’à déclarer l’extension qui me permet d’appeler un poste ainsi, comme ceci :

[phones]

exten => *4042,1,NoOp()
 same => n,Dial(PJSIP/foo,b(handlers^set_auto_answer^1),3)
 same => n,Hangup()

Le paramètre crucial ici étant l’option b(…) donnée à l’application Dial. Avec ceci, on devrait désormais avoir un interphone fonctionnel.

Une limitation de ce système que je trouve un peu frustrante, c’est que je n’ai pas d’indication claire du moment exact à partir duquel je peux parler. C’est d’autant plus problématique quand j’appelle depuis ou vers un téléphone Yealink, parce qu’ils sont lents.

Diffusion globale

Maintenant qu’on a une fonction d’interphone, on peut s’en servir pour en faire un système de diffusion globale, aussi appelée « paging » en anglais.

Cela consiste à définir un numéro qui, une fois composé, permet de diffuser une annonce audible dans un bâtiment entier (par exemple, dans un magasin) en parlant dans le combiné du téléphone.

Pour cela, il faut utiliser l’application Page au lieu de Dial, car Page crée une conférence ad hoc dans laquelle sont placés tous les téléphones appelés, alors que Dial aurait simplement provoqué la diffusion sur un téléphone au hasard. Les options sont très proches. J’ai choisi l’extension *4000 pour qu’il soit facile à mémoriser. Dans extensions.conf, voilà ce que ça donne :

[globals]

; Liste de tous les téléphones prenant en charge le décroché auto
PAGE_ALL_PHONES => PJSIP/foo&PJSIP/bar&PJSIP/baz&…

[phones]

exten => *4000,1,NoOp()
 same *> n,Page(${PAGE_ALL_PHONES},b(handlers^set_auto_answer^1)di,3)
 same => n,Hangup()

Je laisse trois secondes à tous les téléphones pour prendre l’appel. Ce délai permet de gérer correctement le cas d’un téléphone déjà en communication : dans ce cas, le téléphone sonne en double appel, mais cet appel est abandonné après quelques secondes chez ceux qui n’ont pas pu le prendre directement sur haut-parleur. J’ai procédé par tâtonnements pour trouver le délai idéal, surtout à cause de mes Yealink, qui mettent beaucoup de temps avant de réagir à un message INVITE. Un délai de 2 s s’est avéré insuffisant, par exemple, mais 3 s semble être plus fiable.

Je me donne la possibilité d’avoir du son bidirectionnel avec l’option d. Avec cette option, Asterisk mixe ensemble le son capté par l’ensemble des téléphones appelés et envoie le résultat à l’appelant, ce qui permet à l’appelé, où qu’il soit, de répondre comme pour un appel interphone classique. Attention cependant à l’effet Larsen quand on teste avec plusieurs combinés proches les uns des autres ou quand plusieurs postes se trouvent dans la même pièce !

Le résultat ne me permet que des annonces en direct. On pourrait imaginer une version plus avancée qui permette d’enregistrer un message, de le réécouter puis de diffuser ce message après validation, comme ce que permettent certains systèmes commerciaux, mais je n’ai jusqu’ici pas eu besoin d’une application aussi complexe.

Rappels automatiques

Des circonstances particulières font que ma bien-aimée a ressenti le besoin d’avoir un système de rappels à des heures fixes. Comme elle n’a pas et ne veut pas de smartphone, je me suis dit que je pouvais rapidement bricoler un système utilisant le service de diffusion globale que j’avais déjà avant : comme ça, au moins, je suis certain que le message atteindra sa cible à l’heure souhaitée.

Ma solution est mise en œuvre par une tâche cron, qui exécute un script shell, qui dépose dans le répertoire /var/spool/asterisk/outgoing un fichier d’appel (call file) spécial. Ce fichier décrit un appel émis par un pseudo-canal local vers le service de diffusion générale que j’avais déjà.

Tout se passe dans un contexte dédié que j’ai appelé reminder et qui se programme de la façon suivante :

[reminder]

exten => broadcast,1,NoOp()
 same => n,Page(${PAGE_ALL_PHONES},b(handlers^set_auto_answer^1)iq,3)

exten => play_message,1,NoOp()
 same => n,Answer()
 same => n,Wait(2)
 same => n,Playback(reminder)
 same => n,Hangup()

L’application Playback joue un fichier appelé reminder, préalablement enregistré par mes soins et placé dans le répertoire des sons d’Asterisk.

Ensuite, le fichier d’appel en question a le contenu suivant :

Channel: Local/play_message@reminder
Context: reminder
Extension: broadcast 
Callerid: Rappel automatique <reminder>

La variable Channel d’un fichier d’appel indique normalement le poste qu’Asterisk fait préalablement sonner. Une fois décroché, ce canal est alors envoyé vers le triplet Content, Extension et Priority donné en argument (et Priority vaut 1 par défaut). Dans notre cas, on utilise un canal « local », qui simule ici une personne ayant appelé le service de diffusion globale, attend deux secondes (pour laisser le temps aux Yealink de réagir) et qui parle dans son combiné avant de raccrocher.

Enfin, le script appelé par la tâche cron est assez simple. Une façon de faire (qui nécessiterait les droits root, donc l’idéal serait que ce soit fait sous un autre utilisateur non privilégié, éventuellement avec des droits sudo) serait de procéder ainsi :

install -m 600 -o asterisk -g asterisk \
        /chemin/vers/reminder.call \
        /var/spool/asterisk/_tmp_call && \
    mv /var/spool/asterisk/_tmp_call \
       /var/spool/asterisk/outgoing/reminder.call

L’écriture de ce genre de scripts exige une attention particulière car il y un piège : il y a en effet un risque de race condition entre le processus qui écrit le fichier d’appel et Asterisk qui traite un fichier d’appel dès qu’il apparaît dans le répertoire adéquat. Si on ne fait pas attention, Asterisk risque de traiter un fichier tronqué, vierge ou illisible pour cause de droits insuffisants et l’appel échouera. Pour éviter cela, il faut procéder en deux étapes :

  • écrire le fichier dans un répertoire proche, changer le propriétaire et le groupe en asterisk et le mode en 600 (pas besoin de plus) ;
  • quand le fichier est prêt, le déplacer (mv) dans le bon répertoire ; le déplacement d’un fichier étant atomique tant que le répertoire source et destination sont sur le même système de fichiers.

Ce système de rappels automatiques a ainsi fonctionné pendant plus d’un mois sans faillir (excepté au début, lors de sa mise au point, à cause du piège mentionné ci-dessus notamment).

Conclusion

J’ai montré comment construire pas à pas une petite gamme d’applications reposant sur le principe d’appels internes automatiquement pris sur haut-parleur.

La fonction d’interphone est très appréciée par ma compagne, parce qu’elle apporte une alternative au fait de hurler « C’est prêt ! » quand j’ai fini de faire la cuisine qui lui est beaucoup moins désagréable. La diffusion globale lui permet ensuite de n’avoir qu’un seul numéro à mémoriser. Enfin, le rappel périodique en diffusion globale lui a été très utile pendant plus d’un mois et lui sera encore d’une aide très précieuse pour les quelques mois à venir. Voilà donc un exemple concret d’une application intéressante pour un PBX à la maison !

Mais ce n’est pas tout : la souplesse de mon serveur Asterisk, et plus largement le fait d’avoir une infrastructure réseau chez moi qui ressemble peu ou prou à un réseau d’entreprise, m’ont bien servi pendant les épisodes de confinement dues à la pandémie du Covid-19. Mais ceci sera un sujet pour un autre billet de blog.

Des changements dans les coulisses de monrer.fr

 x0r   6
sncf gtfs ratp monrer.fr postgresql

Mise à jour du 14 août 2021 : la migration s’est déroulée sans encombres. Merci pour votre patience !

Mise à jour du 9 août 2021 : ajout d’un paragraphe sur les numéros des trains du RER D (sujet que j’avais oublié de traiter dans une version précédente de l’article).

Ça fait déjà huit ans depuis que j’ai ouvert le site monrer.fr et il était temps que je reprenne un peu ce vieux projet, sur lequel j’avais communiqué pour la dernière fois en novembre 2015, pour y corriger de vieux problèmes latents.

Il m’a fallu beaucoup de temps pour retrouver ma motivation pour faire évoluer le site. D’abord parce que pendant longtemps, je n’avais plus trop envie de coder sur mon temps libre : c’était déjà mon activité principale pendant mes horaires de travail. D’autres limitations imposées de l’extérieur m’ont également frustré (et me frustrent encore), comme le quota d’appels de l’API Temps Réel Transilien qui est de 20 par minute et dont j’ai déjà parlé auparavant.

Mais plusieurs changements plus ou moins récents m’ont redonné suffisamment d’inspiration pour revenir sur ce projet.

Premièrement, chez mon employeur, je suis passé du domaine du développement logiciel à celui de l’ingénierie système il y a quelques années. Deuxièmement, j’ai découvert l’excellent ouvrage The Art of PostgreSQL (2e édition) de Dimitri Fontaine : les exemples m’ont émerveillé, et ce dès la page 16. Troisièmement, pour la première fois depuis la fin de mes études, j’ai pris trois vraies semaines de congé consécutifs.

Enfin, et pas des moindres : pendant ma première semaine de ces congés, un utilisateur de monrer.fr, qui me contacte déjà régulièrement lorsque le site rencontre un problème, m’a signalé un nouveau souci. J’avais en effet un bug dans une requête SQL qui doit récupérer les horaires théoriques de passage des prochains trains à une gare donnée et à un instant donné ; ce bug ne se manifestait que dans des cas très précis, notamment lors de services dégradés mis en place durant des phases de travaux estivaux. Alors quitte à remettre la main à la pâte pour corriger ce bug en particulier, autant en profiter pour résoudre d’autres problèmes plus structurels dus à des choix techniques plus ou moins discutables que j’ai faits il y a huit ans.

Accélération de la recherche des horaires théoriques

Par exemple, la recherche des horaires théoriques prenait une à deux secondes en MySQL, ce qui mettait parfois la machine à genoux durant des périodes de forte charge. Mes nombreuses soirées passées à décortiquer les sorties de commandes EXPLAIN et à indexer mes tables ont été autant d’efforts qui ne se sont pas traduits par des gains de temps spectaculaires. Par ailleurs, je souhaitais aussi avoir non seulement la liste des trains passant à une gare donnée, mais aussi, pour chaque train, la liste complète des prochains arrêts et le nom de son terminus, ce que je faisais en exécutant six fois une autre requête.

Mais en PostgreSQL, entre autres grâce à la fonction array_agg() qui me permet d’agréger un ensemble de données dans une colonne de type tableau, et aux window functions, qui me permettent d’examiner les tuples voisins dans le résultat de la même requête, je suis parvenu à une seule requête qui, en seulement 40 ms, me donne toutes les informations d’un seul coup. L’introduction d’un seul index a été décisif pour accélérer cette requête.

Par conséquent, les temps de chargement devraient être considérablement réduits pour les gares des lignes A et B, pour lesquels je ne dispose pas encore des horaires temps réel. Mais ça aussi, ça va changer.

Obtention d’accès pour l’API Temps Réel de la RATP

En effet, un autre défaut latent de monrer.fr est l’absence d’horaires temps réel pour les lignes A et B du RER. À l’époque, la RATP ne les mettait pas encore à disposition. Ça a changé depuis plusieurs années déjà, donc il ne me restait plus qu’à faire les démarches pour obtenir les accès à l’API Temps Réel RATP. Ce ne sera pas encore pour tout de suite, car l’intégration de cette API avec mon code existant est une tâche qui s’annonce complexe : la FAQ explique par exemple que le rapprochement entre les résultats de cette API et les horaires GTFS doit être faite d’après les noms des gares ; une solution qui ne me plaît guère.

Unification des gares

J’avais fait jadis l’hypothèse que chaque gare répertoriée dans mon site a un unique code TR3 (à trois lettres) et un seul code UIC (sept ou huit chiffres, utilisé comme clef par l’API Temps Réel Transilien). Cette hypothèse s’est rapidement avérée fausse, notamment :

  • lorsque la même gare dessert le RER A ou B (RATP) d’une part et une ligne RER ou Transilien exploitée par la SNCF d’autre part, comme Massy – Palaiseau ou Massy – Verrières ;

  • dans le cas des grandes gares parisiennes, où la partie « grandes lignes » et la partie « banlieue » ont des codes UIC différents, comme à Paris Gare de Lyon ;

  • ou encore quand on cumule ces deux propriétés, comme Paris Nord.

J’avais d’abord contourné le problème en dédoublant les gares concernées, ce qui n’a jamais marché de façon satisfaisante. Il fallait donc absolument revenir sur cette hypothèse.

Pour cela, il fallait donner à chaque gare un identifiant neutre, pouvant être associé à un ou plusieurs codes UIC mais servant de point d’entrée principal pour identifier une gare du point de vue d’un voyageur.

Par conséquent, il n’y a plus besoin de choisir entre « Massy – Palaiseau RER B » et « Massy – Palaiseau RER C » lorsqu’on veut les prochains départs à la gare de Massy – Palaiseau : il n’y a plus qu’un seul « Massy – Palaiseau » et les résultats peuvent ensuite être filtrés par ligne comme n’importe quelle autre gare.

Démonstration des gares unifiées
Principale conséquence de cette unification : ce genre d’horreurs fera partie du passé.

Bascule du format des horaires GTFS

J’en profite aussi pour basculer sur le nouveau format GTFS que la SNCF propose depuis peu pour son export des horaires théoriques. La loi d’orientation des mobilités (LOM) les ont en effet amenés à bifurquer cet export GTFS en un « ancien format » et un « nouveau format ».

À première vue, le nouveau format semble plus facile à exploiter pour moi : les numéros des trains (six chiffres pour un train d’une ligne SNCF et quatre lettres + deux chiffres pour un train d’une ligne RATP) sont désormais renseignés dans le champ trip_short_name de la table trips. Je n’ai donc plus besoin de faire de honteux bidouillages avec les identifiants trip_id comme je le faisais jadis. De toute manière, ces trip_id sont devenus des identifiants opaques.

Le seul inconvénient est que le rapprochement avec les gares est un peu plus compliqué car les identifiants des gares (stop_id) ne sont plus dérivés des codes UIC, mais il est possible d’obtenir une table de transcodage entre les identifiants qu’on trouve dans le GTFS et le code UIC.

Quant aux trigrammes des gares, il existe maintenant une source en Open Data : le Lexique des abréviations SNCF. La liste n’est pas complète, mais j’ai néanmoins pu corriger quelques gares prises au hasard pour lesquels le code à trois lettres était incorrect ou m’était inconnu. Mais le rapprochement entre ma propre base de données et le lexique officiel afin de vérifier et éventuellement corriger toutes les gares qui y sont répertoriées sera un projet pour plus tard.

Affichage correct des numéros de train du RER D

En règle générale, les trains ayant un numéro impair s’éloignent de Paris et ceux ayant un numéro pair s’en rapprochent. Ce qui donne aussi le nom au sens de circulation sur une ligne de chemin de fer, qu’on désigne par « pair » et « impair ».

Mais les numéros des trains du RER D sont particuliers car les trains traversant Paris changent du numéro entre Paris Gare de Lyon et Châtelet – Les Halles, un fait qui m’a longtemps posé problème pour le rapprochement entre horaires temps réel et horaires théoriques (et qui, visiblement, a aussi provoqué des dysfonctionnements du côté des vrais écrans Infogare il y a longtemps). Par exemple, un train à destination de Villiers le Bel, portant le numéro 126658 à son départ de Corbeil-Essonnes, devient le 126659 lorsqu’il quitte la Gare de Lyon pour Châtelet – Les Halles. De même, le train 153692 partant de Goussainville à destination de Melun devient le 153693 lorsqu’il quitte Châtelet – Les Halles pour la Gare de Lyon.

Pendant longtemps, pour ces trains-là, l’API Temps Réel Transilien me donnait le numéro du train valable à la gare pour laquelle je demandais les horaires : tantôt 126658, tantôt 126659 pour le premier exemple. Mais depuis le mois de novembre 2019 environ, l’API renvoie le numéro composite 126658-126659, ce à quoi j’ai réagi par un bricolage où je gardais systématiquement le numéro pair. Depuis, les numéros des trains affichés pour les gares du RER D n’étaient pas toujours les bons.

Afin de faire les choses proprement, j’ai dû ajouter aux gares de la ligne D deux informations : un numéro d’indice unique au sein de la ligne, où ma convention a été de choisir des numéros croissants dans le sens nord-sud et un booléen qui indique un sens impair dans le sens des numéros croissants s’il est vrai et un sens pair sinon ; ce booléen est donc faux pour Châtelet – Les Halles et toutes les gares au nord de celle-ci et vrai pour Paris Gare de Lyon et toutes les gares au sud de celle-ci. Avec ceci, il m’est possible de déterminer la « parité » d’un train au passage d’une gare donnée, puis lequel des deux numéros, pair ou impair, je dois afficher. Avec ceci, les numéros des trains du RER D sont de nouveau affichés correctement.

Par ailleurs, le rapprochement entre horaires temps réel et théoriques est simplifié car les exports GTFS utilisent aussi ces numéros de train composites.

J’estime aussi que ce cas, particulier au RER D à l’heure où j’écris ces lignes, pourrait aussi concerner le RER E lorsqu’il sera prolongé vers l’ouest. Dans ce cas, je suppose que les trains changeront de parité en quittant Haussmann Saint-Lazare.

Migration prévue le week-end du 15 août

Je souhaite vous faire profiter de mes modifications le plus vite possible, mais je sais que la bascule ne se fera pas en un claquement de doigts. C’est pourquoi je serai contraint de mettre le site hors ligne quelques instants durant le week-end du 15 août prochain. Je m’excuse donc par avance des désagréments que pourraient causer cette courte indisponibilité.

Du Lisp

 x0r   0
lisp

Je programme depuis l’âge de sept ans et au cours de ma vie, j’ai eu l’occasion et la curiosité d’essayer et d’utiliser de nombreux langages de programmation. Mais parmi les langages que je connais, le Lisp est celui qui a de loin le plus bousculé ma vision de la discipline.

J’ai découvert le Lisp de manière complètement fortuite il y a deux ans. Je cherchais en fait un outil de mind-mapping et j’étais alors tombé sur Org mode couplé à Spacemacs, lui-même une surcouche pour GNU Emacs.

De fil en aiguille, Emacs m’a fait découvrir Emacs Lisp. À partir de là, j’ai fini par adopter Common Lisp et dans une moindre mesure Clojure, en essayant au passage Haskell (qui n’est certes pas un Lisp, mais semble faire partie de la famille en tant que membre d’honneur). Mais ma dernière découverte, et de loin la plus étonnante, est le langage Racket, que j’étudie actuellement.

Au cours de ces différents essais de langages, ma bien-aimée a essayé et adopté Common Lisp plutôt que le C pour une idée de jeu qu’elle avait depuis plus de dix ans et j’ai aussi fait des prototypes de générateurs de texte à base de chaînes de Markov et un solveur de picross.

La famille Lisp

Le Lisp d’origine date de la fin des années 1950, ce qui fait de la famille des Lisp une des plus anciennes qui existent.

Il est difficile d’illustrer ce qui m’attire dans les Lisp sans donner au moins un exemple de code. En voici un, exprimé en Common Lisp, qui calcule la factorielle d’un nombre n en utilisant une fonction auxiliaire f et une récursion terminale :

(defun factorial (n)
  "Calcule la factorielle du nombre N."
  (flet ((f (acc n)
           (if (<= n 1)
               acc
               (f (* acc n) (1- n)))))
    (f 1 n)))

Je déclare ici une fonction factorial, acceptant un seul paramètre nommé n, accompagné d’une petite description destinée à un système de génération documentaire. J’y introduis une fonction auxiliaire f prenant en paramètre un résultat pratiel acc (pour « accumulateur ») et le nombre dont on calcule la factorielle, toujours appelée n.

Dans cette fonction auxiliaire, si n est inférieur ou égal à 1, le calcul est terminé et on renvoie acc. Sinon, on appelle f récursivement. Avec cette fonction auxiliaire définie, il ne reste plus qu’à l’appeler avec 1 et n comme paramètres pour calculer le résultat.

Ce qui saute immédiatement aux yeux sont la quantité de parenthèses. Il s’agit en fait de la représentation canonique de listes, qui sont délimitées par des parenthèses et dont chaque élément est séparé par un blanc. On dit aussi que ces listes sont représentées sous la forme de S-expressions.

Ainsi, (f (* acc n) (1- n)) est une liste dont les éléments sont, dans l’ordre, le symbole f, la liste (* acc n) et la liste (1- n). Et puisque chaque élément d’une liste peut aussi être une liste, on peut utiliser ces listes pour construire un arbre syntaxique d’un programme.

Voilà donc une des différences fondamentales entre le Lisp et les autres langages de programmation : en Lisp, le code source est une représentation explicite de son arbre syntaxique, alors que dans les autres langages, cet arbre, généralement construit et manipulé dans le compilateur uniquement, est inaccessible de l'extérieur.

Voici un second exemple en Common Lisp qui affiche une liste de courses :

(defun courses (ingredients)
  "Affiche une liste de courses."
  (if (null ingredients)
      (princ "Je n’ai rien à acheter")
      (progn
        (format t "J’ai ~D ~A à acheter : "
                (length ingredients)
                (if (> (length ingredients) 1) "choses" "chose"))
        (labels ((p (ingredients)
                   (format t "~(~A~)" (first ingredients))
                   (case (length ingredients)
                     (1 (princ "."))
                     (2 (princ " et ") (p (rest ingredients)))
                     (otherwise (princ ", ") (p (rest ingredients))))))
          (p ingredients))))
  (fresh-line))

(Note aux connaisseurs : je sais que j’aurais pu me contenter d’un unique appel à la fonction format pour afficher cette liste, mais la chaîne de format deviendrait très vite absconse.)

Appeler cette fonction depuis du code nécessite cependant d’utiliser un opérateur spécial, quote, pour indiquer une donnée à utiliser telle quelle : un symbole qui ne désigne pas une variable ou une liste qui n’est pas un appel de fonction. Ici, il faut que la liste de courses soit traitée comme une liste au lieu d’un un appel de fonction (notez l’apostrophe devant chaque liste, qui est la forme abrégée de quote), comme ceci :

* (courses '())
Je n’ai rien à acheter

* (courses '(œufs))
J’ai 1 chose à acheter : œufs.

* (courses '(œufs chocolat))
J’ai 2 choses à acheter : œufs et chocolat.

* (courses '(œufs chocolat sucre farine))
J’ai 4 choses à acheter : œufs, chocolat, sucre et farine.

Le fait qu’un programme Lisp soit représenté à l’aide de sa propre représentation de listes et d’arbres est aussi appelé homoiconicité. Cette propriété, partagée par très peu de langages de programmation, rend ainsi trivial la génération de code. Par conséquent, Lisp bénéficie d’un système de macros parmi les plus puissants de tous des langages de programmation.

Emacs et Emacs Lisp : ma première expérience de Lisp

J’avais déjà utilisé un peu Emacs quand j’étais en école d’ingénieurs, mais je ne me voyais pas m’en servir autrement qu’après avoir tapé M-x viper-mode car j’avais beaucoup trop l’habitude des touches de vi. Spacemacs me retirait une grosse épine du pied en proposant un Emacs « vimifié » ; j’étais donc prêt à me refaire une nouvelle première impression. Et j’allais avoir une semaine de vacances devant moi pendant lesquelles j’aurais suffisamment de temps pour explorer sérieusement Emacs.

Je me souviens donc bien comment je m’étais installé dehors, un après-midi, avec mon PC portable sur les genoux, pour lire le manuel d’Emacs Lisp.

Les extensions d’Emacs, mais aussi une grosse partie de l’éditeur lui-même, sont codés en Emacs Lisp. Ce langage est un dérivé d’un dialecte assez ancien de Lisp, Maclisp, qui date des années 1960. Ce qui explique certains de ses pièges, comme le fait que les variables aient par défaut une portée dynamique plutôt que lexicale (ce qui est cependant en train de lentement changer). La quantité de code Emacs Lisp qui circule de nos jours signifie que tout changement majeur, comme une forme de programmation concurrente, doit être introduite en procédant à tâtons pour éviter de casser le code existant.

Pour cette raison, les détracteurs disant qu’Emacs a davantage les fonctionnalités d’un système d’exploitation que ceux d’un éditeur de texte ont à mon avis partiellement raison. Emacs est en effet un interpréteur Lisp doté de fonctions conçues pour éditer du texte, mais il est capable de bien plus que ça. Autrement dit, Emacs est une machine Lisp moderne.

J’utilise maintenant énormément Org-mode au travail et je me suis fait quelques petits scripts en Emacs Lisp pour automatiser certaines tâches administratives fastidieuses. Étant contraint à Windows au bureau, système sur lequel Emacs a été porté, j’apprécie beaucoup cet accès facile à un Lisp tout à fait honorable. Et en parallèle, je suis devenu encore plus frustré par les outils informatiques dépourvus du moindre mécanisme d’extension.

Toujours est-il que l’omniprésence d’Emacs Lisp dans Emacs font de cet éditeur un des meilleurs outils pour programmer non seulement en Emacs Lisp, mais dans n’importe quel Lisp de manière générale.

Common Lisp : l’héritier des Lisp traditionnels

Le Common Lisp a été l’étape suivante dans mon aventure. Mon tutoriel a été le livre Practical Common Lisp de Peter Seibel, qu’on peut lire gratuitement en ligne ou acheter en version papier. Et quand on code, le Common Lisp HyperSpec est accessible en ligne et constitue le manuel de référence du langage.

Ce langage est le fruit d’un effort de standardisation d’un Lisp remontant aux années 1980. Ces efforts ont abouti à une norme ANSI publiée en 1994 et restée inchangée depuis. Le but du jeu était de créer un Lisp couvrant les fonctionnalités de plusieurs dialectes incompatibles entre eux. Par conséquent, le langage n’est certes pas aussi « pur » qu’un Scheme et comporte quelques petites incohérences et de petits défauts ; la syntaxe de la macro loop et celle de la fonction format étant les aspects les plus controversés. Néanmoins, il est tout à fait apte à être utilisé pour des applications industrielles et j’ai même vu un livre de finance dont les exemples de code sont en Common Lisp. Paul Graham explique par exemple comment, dans les années 1990, il a monté une start-up d’hébergement de boutiques en ligne, Viaweb, en Lisp, dans son article « Beating the Averages ».

Son écosystème riche, son système de programmation orientée objet, CLOS, ainsi que son système de gestion d’erreurs qui propose une approche intéressante aux exceptions tels qu’on les trouve en C++, Java ou Python font de ce Lisp un langage qui vaut le détour.

Clojure : un Lisp pour la JVM

Clojure, quant à lui, est un Lisp plus récent, conçu pour s’intégrer dans l’écosystème Java. Il s’exécute en effet exclusivement dans la JVM et le langage est bien entendu doté de primitives pour interagir avec des classes Java. En résumé, c’est une façon de faire du Java sans subir la syntaxe verbeuse ni l’orienté objet à outrance du Java.

Son Java interop m’a déjà bien servi : j’avais eu besoin un jour de déboguer un document Word (au format docx), généré par un logiciel mais que Word déclarait corrompu. Pour cela, j’ai utilisé la bibliothèque Java docx4j, quelques fonctions en Clojure mais surtout le REPL de Clojure pour explorer la structure du document et trouver le problème. In fine, il s’avérait juste que deux éléments XML avaient le même identifiant.

Il existe aussi une variante de Clojure appelée ClojureScript, qui comme son nom l’indique tourne non pas sur la JVM mais dans un environnement JavaScript. Clojure et ClojureScript utilisés ensemble permettent donc de partager du code entre les côtés serveur et client dans un projet Web, mais je n’ai pas encore eu l’occasion de mettre cela en pratique. Cela étant, le développement en ClojureScript pour cibler un navigateur est un peu plus fastidieux car l’accès à un REPL est plus compliqué à mettre en place qu’avec Clojure.

Parmi les aspects les plus intéressants du langage figurent les lazy seqs, c’est-à-dire des séquences, pouvant être infinies, qui sont évalués de façon paresseuse. On peut par exemple définir une fonction qui génère la suite (infinie) de Fibonacci, puis n’en demander que le dixième terme ; ceci provoque le calcul des dix premiers termes uniquement.

Mais il y a plusieurs aspects qui me rebutent dans ce langage et qui me font hésiter à en faire mon outil de choix.

Premièrement, du fait de son adhérence forte à Java, certains des reproches qu’on fait à Java s’appliquent aussi à Clojure. Lancer un REPL, par exemple, devient vite lent et gourmand en RAM : dans un projet vierge, ça démarre en une demi-seconde avec déjà 294 Mo de RAM occupés, mais dans un projet plus avancé avec une quinzaine de bibliothèques en dépendances, ça monte à 678 Mo de RAM après près de deux minutes de compilation. Et dans mon environnement Emacs, il n’est pas rare de voir des sous-processus Java monter à plus de 2 Go de RAM !

Clojure me semble plutôt marcher sur les plates-bandes traditionnelles de Java, qui sont les gros logiciels (trop) complexes qu’on démarre et arrête généralement en même temps que sa machine hôte. Je ne me vois pas faire de scripts avec, simplement à cause du temps de démarrage de la JVM qui interdit de fait ce type d’usages.

Deuxièmement, le fait qu’il soit distribué sous la licence Eclipse Public License (EPL) 1.0 me semble être un choix malheureux, étant donné qu’il faut nécessairement distribuer le binaire (.jar) du cœur de Clojure avec tout programme Clojure. Or cette licence connue pour poser problème avec d’autres licences libres comme la GPL.

Le langage est en lui-même intéressant et a peut-être permis d’introduire du Lisp dans des environnements traditionnellement prompts à se ruer sur Java pour tout projet. Mais ce ne sera pas le langage que je choisirai en premier pour un nouveau projet car ça reste une « usine à gaz ».

Haskell, le parfait anti-Lisp dans sa syntaxe

Certains ouvrages sur Lisp que j’ai lus avant d’écrire ce billet mentionnaient Haskell, presque comme s’il s’agissait d’un Lisp lui aussi. Il s’agit d’un langage fonctionnel, une approche qui se prête bien au Lisp. Par curiosité, j’ai voulu essayer.

Le tutoriel Web sur le site officiel, entièrement interactif, m’a particulièrement impressionné. Une autre très bonne ressource qui m’a servi pour essayer le Haskell est le livre Learn You a Haskell for Great Good! de Miran Lipovača (disponible gratuitement en ligne).

Mais j’ai du mal avec sa syntaxe. La quasi-absence de parenthèses est très déroutante car elle oblige le lecteur à se rappeler des règles d’associativité. Les appels de fonction n’échappent pas à la règle ; le langage doit donc prévoir un opérateur, $, pour appliquer une fonction avec une associativité à droite plutôt qu’à gauche ; le tout est un obstacle à un code intelligible. Ensuite, la nécessité d’indenter le code de façon très précise, ce qui revient à donner une importance syntaxique aux blancs (comme en Python), est un autre aspect avec lequel j’ai personnellement beaucoup de mal. Le haskell-mode d’Emacs facilite un peu les choses, mais j’ai tout de même le sentiment que ce langage n’est pas fait pour moi.

Néanmoins, je perçois l’influence du Lisp dans le fait que l’opérateur servant à ajouter un élément au début d’une liste (:) s’appelle « cons », comme la fonction du même nom en Lisp.

J’accorderai peut-être une seconde chance plus tard à ce langage, mais dans l’immédiat, je préfère le confort et l’inambiguïté des S-expressions.

Retour à Common Lisp

En recherchant des ressources sur la programmation en Emacs Lisp, je suis tombé sur un petit tutoriel ludique dans lequel on programme un petit jeu d’aventure textuel en Lisp. Ce tutoriel est par ailleurs une bonne démonstration de ce qu’il est possible de construire avec des macros Lisp.

L’auteur, Conrad Barski, a ensuite publié le très bon ouvrage Land of Lisp, qui va dans le prolongement de ce petit tutoriel : tous les exemples sont des jeux. Cette approche convient particulièrement bien à un public non informaticien et permet d’aborder des thématiques que je ne ne vois pas souvent dans des introductions à des langages de programmation, comme par exemple la programmation d’intelligences artificielles pour des jeux de plateau. Grâce à ce livre, Nausicaa a adopté le Common Lisp et est en train d’écrire un jeu dans ce langage ; ce qui est de très bonne augure, car cela nous fait un langage de programmation en commun que nous apprécions ensemble.

Peu avant de partir en vacances d’été, je me suis amusé, en guise de petit défi, d’écrire un solveur de picross. J’avais choisi Common Lisp plutôt que Clojure car Common Lisp ne dispose pas des lazy seqs de Clojure. C’est un projet qui s’est avéré beaucoup plus intéressant qu’à première vue et dont je parlerai sûrement dans un billet futur.

Par ailleurs, Common Lisp étant lui-même une spécification, il existe en fait plusieurs environnements Lisp qui portent des noms différents : parmi eux, il y a GNU CLISP, SBCL, Clozure CL (à ne pas confondre avec Clojure) et ABCL, ce dernier ayant la particularité de tourner sur la JVM.

Racket ou la programmation orientée langages

Ma dernière destination dans mon voyage dans l’univers des Lisp est Racket.

Racket est en fait un langage dérivé de Scheme, qui est lui-même un Lisp qui a vu le jour dans les années 1970 au MIT. Racket est conçu à la fois comme un outil de recherche et comme un support pédagogique en théorie de langages de programmation (d’où son nom d’origine, PLT Scheme – PLT pour Programming Language Theory).

La principale caractéristique de Racket est d’offrir un cadre pour définir, dans le compilateur Racket, n’importe quel langage dédié. Bien que les autres Lisp se prêtent très bien à la création de tels langages dédiés, ces langages sont la plupart du temps tenus à respecter la syntaxe des S-expressions. Racket, quant à lui, permet de créer des langages qui s’affranchissent de cette limitation : ainsi, on peut très bien créer des langages où on peut exprimer le produit 7 × 191 par une notation infixe (7 * 191) plutôt que préfixe ((* 7 191)).

Ainsi, Racket pousse la notion de programmation orientée langages le plus loin possible. Plutôt que d’exprimer une solution à un problème directement dans un langage de programmation, on définit d’abord un langage plus concis dans lequel on exprime le problème, on écrit un programme transformant les expressions de ce nouveau langage en code Racket, puis on compile ce nouveau langage.

La programmation orientée langages a lui-même de nombreuses applications. Il existe par exemple un langage d’édition vidéo. Le studio Naughty Dog a également utilisé des mini-langages créés dans Racket pour scripter l’intrigue et l’intelligence artificielle dans certains de ses jeux (voir par exemple cette présentation par le CTO de l’entreprise).

J’ajouterais aussi que même si personnellement, je préfère Emacs et son racket-mode pour des raisons de confort, l’IDE de référence, DrRacket est bien conçu et est un outil qu’on peut mettre entre toutes les mains, notamment de débutants. En particulier, le livre How to Design Programs par Matthias Felleisen et al. utilise Racket comme outil pédagogique.

Je n’ai pas encore eu l’occasion de me faire la main sur Racket avec un projet sérieux, mais je vois énormément de potentiel dans un langage qui a l’air à la fois très puissant et très accessible à des débutants.

Conclusion

Ma découverte fortuite d’Emacs m’a permis d’explorer en deux ans un univers entier de langages de programmation que je n’aurais pas eu la curiosité d’étudier sinon. Je suis conscient que je n’ai pas encore exploré Scheme, en dépit de ma possession d’un exemplaire papier de Structure and Interpretation of Computer Programs de Harold Abelson et Gerald J. Sussman (consultable gratuitement en ligne), ni Guile, entre autres utilisé pour scripter divers logiciels GNU. Mais je pense néanmoins avoir au moins fait le tour des langages les plus représentatifs de ce qu’est la famille Lisp aujourd’hui.

Ces nouveaux langages pour moi ont été l’occasion de me lancer dans certains projets personnels dont je parlerai sûrement dans d’autres billets de blog : par exemple, j’ai fait un robot générateur de texte à base de chaînes de Markov ou un solveur de Picross assez efficacement ; dans les deux cas, mon tout premier prototype était opérationnel en l’équivalent d’une ou deux soirées.

Me voilà donc converti au Common Lisp, voire peut-être au langage Racket si mes expériences dans ce langage-là sont concluants.

Pour terminer, je me permets de citer Eric S. Raymond, qui, dans son essai intitulé « How To Become A Hacker », écrit :

LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot.

Thèse d’histoire en LaTeX : quelques trucs et astuces

 x0r   0
latex histoire tex thèse

Dans mon précédent billet sur la thèse de Nausicaa préparée en LaTeX, je vous avais promis de partager quelques « astuces [de LaTeX] que j’ai découvertes au fil de l’eau ». Ce billet vise donc à tenir cette promesse.

Ce que je présente ici sont quelques personnalisations, surtout de forme, qui étaient à la fois non-triviales (c’est-à-dire qu’il n’existait pas de commandes dans la documentclass memoir pour le faire facilement) et indispensables pour que la thèse puisse être acceptée et soutenue. C’est en outre le fruit de la lecture de moult références, sur TeX, LaTeX et BibLaTeX notamment.

Je vous propose donc quelques « recettes » de LaTeX, commentées comme il se doit pour ne pas me contenter d’être une compilation de bouts de code à copier-coller dans un projet LaTeX et illustrer comment on peut construire des fonctionnalités avancées. C’est pourquoi notre progression se fera dans un ordre croissant de difficulté.

Préliminaires

Les extraits de code que je présente partent du principe que :

  • votre document utilise la documentclass memoir (et non pas book) ;
  • et votre fichier .tex principal inclut, dans son préambule, un fichier .sty contenant toutes vos commandes de personnalisation (de forme) à l’aide d’un \usepackage.

Comme je l’avais déjà expliqué dans mon précédent billet, memoir est strictement supérieur à book car le premier propose davantage de possibilités de personnalisation que le second. Le mode d’emploi de memoir est par ailleurs très bien fait et je recommande fortement au moins de le survoler.

Citations au format ISO 690

L’université de la Sorbonne exige que les références bibliographiques soient au format NF Z 44-005. Il s’agit d’une norme, annulée depuis, et remplacée par la norme ISO 690.

BibLaTeX embarque un style ISO 690, mais à l’époque où Nausicaa finissait sa thèse, ce module avait deux problèmes.

Tout d’abord, il manquait encore les traductions françaises pour les petits bouts de texte comme « supervisé par » ou « sous la direction de ». Du moins, ce n’était pas le cas dans la version 0.3.1 que nous utilisions à ce moment-là. Heureusement, ces traductions ont été introduites depuis et devraient être disponibles d’emblée à partir de la version 0.4.0.

Enfin, Nausicaa voulait aussi que les titres des articles apparaissent entre guillements. Pour cela, le seul correctif est de fournir à BibLaTeX un style de bibliographie personnalisé, dérivé du style officiel mais remplaçant le formatage des champs adéquats.

Pour cela, on place un fichier iso-custom.bbx dans le même répertoire que le fichier principal, avec le contenu suivant :

\ProvidesFile{iso-custom.bbx}[2020/07/08 v0.1 biblatex bibliography style]

\RequireBibliographyStyle{iso}

\DeclareFieldFormat[article,periodical,inproceedings]{title}{\mkbibquote{#1}}

Sommaire au début et table des matières à la fin

Le format de la thèse exigeait un sommaire et une table des matières : le sommaire étant au début et ne reprenant que les \part et les \chapter et la table des matières étant à la fin et allant jusqu’au niveau \subsubsection. Or LaTeX ne fournit que la commande \tableofcontents pour insérer une table des matières. Pour le sommaire, il faut ruser un peu.

Tout d’abord, on positionne le compteur tocdepth à 3 pour que la table des matières fasse apparaître les titres jusqu’au niveau \subsubsection.

\setcounter{tocdepth}{3}

Ensuite, on définit une commande \printshorttoc qui peut être utilisé dans le fichier .tex principal pour composer le sommaire. La commande fonctionne comme suit :

  1. Sauvegarder l’ancien nom de la table des matières, défini dans la macro \contentsname (par défaut « Table des matières ») pour le changer temporairement en « Sommaire ». De même pour la valeur du tocdepth.
  2. Positionner temporairement le compteur tocdepth à 0 pour se restreindre aux titres de niveau \chapter dans le sommaire.
  3. Réduire sensiblement les espacements verticaux introduits juste avant chaque partie ou chapitre (longueurs définies dans \cftbeforepartskip et \cftbeforechapterskip) : dans le cas contraire, on perd en effet pas mal de place inutilement sur la page.
  4. Imprimer le sommaire avec \tableofcontents.
  5. Enfin, restaurer le \contentsname et le tocdepth.

La commande \printshorttoc est définie comme suit :

\newcommand{\printshorttoc}{%
  \let\origcontentsname\contentsname
  \@tempcnta=\value{tocdepth}
  \renewcommand*{\contentsname}{Sommaire}
  \setcounter{tocdepth}{\z@}
  {%
    \cftbeforepartskip=0.55\cftbeforepartskip
    \cftbeforechapterskip=0.55\cftbeforechapterskip
    % 
    \tableofcontents%
  }%
  %
  \renewcommand*{\contentsname}{\origcontentsname}
  \setcounter{tocdepth}{\@tempcnta}
}

Un \chapter* et \section* améliorés

Un problème récurrent est l’ajout de sections non numérotées mais qui doivent tout de même apparaître dans le sommaire et la table des matières. Or, même dans la documentclass memoir, les commandes \chapter* et \section* ne le font pas.

Une solution possible est de ne plus utiliser les versions étoilées de ces commandes et d’y substituer les siennes. Concrètement, il faut faire trois choses :

  1. Composer la section au bon niveau (chapitre, section…) ;
  2. Ajouter une ligne dans la table des matières (et si on utilise le package hyperref, comme c’était le cas, il faut invoquer la commande \phantomsection immédiatement avant comme expliqué sur StackOverflow) ;
  3. Enfin, réinitialiser les marqueurs gauche et droite (le texte qui se trouve par défaut en haut de chaque page pour rappeler le titre du chapitre et de la section où on se trouve).

Voici le code (copieusement commenté, parce que le langage TeX devient vite illisible) :

\def\@mark@unnumbered@chapter#1{\markboth{#1}{}}
\def\@mark@unnumbered@section#1{\markright{#1}}
\def\@unnumberedsection#1#2{%
  % Composer le \chapter* ou \section* (en fonction de la valeur du paramètre 1)
  \expandafter\csname #1\endcsname*{#2}%
  % Ajout de l’entrée dans la TDM
  \phantomsection%
  \addcontentsline{toc}{#1}{#2}%
  % Appel de \@mark@unnumbered@chapter ou \@mark@unnumbered@section pour
  % réinitialiser les marqueurs
  \expandafter\csname @mark@unnumbered@#1\endcsname{#2}%
}

\newcommand{\addchapter}[1]{\@unnumberedsection{chapter}{#1}}
\newcommand{\addsection}[1]{\@unnumberedsection{section}{#1}}

Métadonnées personnalisées

Afin de composer une belle page de garde avec un titre, le nom de l’auteur et d’autres métadonnées, LaTeX propose les commandes \title, \author et \date, entre autres.

Mais sur la page de garde d’une thèse, doivent également figurer d’autres informations : l’université, l’année de la thèse, le grade visé, la spécialité, les noms du directeur et des membres du jury, etc.

Mon approche consistait donc à redéfinir la commande \maketitle pour faire figurer ces informations. Mais pas question de les coder en dur : afin de faire les choses proprement, on va simplement définir des champs supplémentaires.

Cela nécessite deux choses : une macro qui compose la valeur du champ, initialisée à une valeur par défaut (ou une séquence de commandes affichant un avertissement sur la console, comme le fait \title) et une commande pour affecter une nouvelle valeur à cette macro. Pour un champ appelé foo, on appelle \@foo le « getter » et \foo le « setter », lesquels peuvent être définis ainsi :

\newcommand\@foo{%
  \@latex@warning@no@line{No \noexpand \foo given}}
\newcommand\foo[1]{\gdef\@foo{#1}}

Étant donné le nombre de répétitions de « foo » dans la définition de ces deux macros, on pourrait imaginer une commande qui, avec une invocation comme \DefineMetaData{foo}, ferait ces deux définitions pour nous. Mais comme avec la technique vue ci-dessus, il faudrait une surabondance de \expandafter et de \csname #1 \endcsname. Je n’ai pas eu la patience d’élaborer une telle commande à temps pour le rendu de la thèse, donc je vous laisse proposer une solution.

Notes de bas de page

La numérotation des notes de bas de page est régie par le compteur footnote.

Normalement, LaTeX réinitialise bien les numéros de notes de bas de page au début de chaque chapitre, à condition d’utiliser \chapter. Mais ce n’est pas toujours suffisant, pour deux raisons.

Premièrement, parce que dans la thèse, les parties commencent par une introduction qui précède le début du premier chapitre de la partie. Les numéros des notes de ces introductions poursuivaient alors à partir du chapitre précédent…

Deuxièmement, parce que pour une raison obscure, ce compteur n’était pas non plus réinitialisé lorsqu’on utilise \chapter dans le \frontmatter ou le \backmatter. Ce qui est embêtant lorsqu’il y en a pour une cinquantaine de pages de contenu liminaire, introduction comprise…

Pour corriger cela, on redéfinit \chapter et \part pour s’assurer de remettre ce compteur footnote à zéro quoi qu’il arrive :

\let\orig@chapter\chapter
\def\chapter{\setcounter{footnote}{\z@}\orig@chapter}
\let\orig@part\part
\def\part{\setcounter{footnote}{\z@}\orig@part}

Conclusion

Il va sans dire que personnaliser LaTeX n’est pas forcément à la portée de tout le monde : une partie de la complexité est due à l’art de la typographie, qui est un art en soi ; l’autre partie est due au langage TeX.

TeX, au fond, est en effet un langage de macros ; cette propriété lui permet de facilement bâtir des langages dédiés très puissants entre de bonnes mains. Mais comme tous les langages de ce type, le test et le débogage de macros personnalisés peut vite virer au cauchemar. C’est pour cette raison que dans des langages comme le C ou le Lisp, par exemple, la règle no 1 de l’élaboration de macros est d’essayer de s’en passer.

C’est pourquoi une compréhension simultanée de TeX et de LaTeX est indispensable si on veut des documents qui sortent de l’ordinaire. Plus exactement, il faut comprendre à la fois le fonctionnement de TeX en tant que langage de programmation (c’est-à-dire les règles syntaxiques, les règles d’expansion…) et le fonctionnement de TeX en tant que typographe (comme la différence entre les modes horizontaux et verticaux, l’algorithme de césure…). Le TeXbook de Donald Knuth explique très bien les deux aspects de TeX et est donc un ouvrage à avoir sous la main à tout moment. Après tout, on a vite fait de sortir du domaine de la typographie pour rentrer dans celui de la programmation.