Programmation en Lua dans LaTeX : affichage de triplets pythagoriciens, II
par Damien Mégy latex lua lualatex maths pdf tikz pgf pdfliteral pdfextension
Table des matiĂšres
Dessiner 1000 fois plus vite que TikZ avec les primitives PDF
Dans l'article précédent, nous avons expliqué comment utiliser Lua avec LuaLaTeX pour calculer et dessiner les points correspondant aux triplets pythagoriciens, en générant dynamiquement du code TikZ depuis Lua. L'objectif était d'obtenir l'image suivante :
L'utilisation du langage de programmation Lua, par opposition à une programmation avec des macros LaTeX, présentait déjà plusieurs avantages : un code plus lisible qu'en TeX pur, des calculs rapides grùce à Lua, et une séparation claire entre calcul et rendu.
Cependant, le dessin des milliers (voire millions) de points avec TikZ est extrĂȘmement lent, mĂȘme lorsque le calcul est optimisĂ© : le rendu avec TikZ monte facilement Ă plusieurs secondes voire dizaines de secondes.
Dans cet article, nous allons voir comment accélérer drastiquement le dessin, jusqu'à obtenir un rendu environ 1000 fois plus rapide. Pour cela, on abandonne TikZ et on utilise directement les primitives PDF.
Pourquoi TikZ est-il lent ?
TikZ est un outil formidable, mais chaque instruction TikZ est analysée par TeX, déclenche des macros complexes et génÚre plusieurs couches d'abstraction avant d'aboutir au PDF final.
Lorsque l'on dessine quelques dizaines ou centaines d'objets, cela ne pose aucun problÚme. Lorsque l'on dessine des dizaines de milliers de petits rectangles, TikZ devient le goulet d'étranglement.
Primitives PDF
LuaLaTeX donne accĂšs Ă une primitive extrĂȘmement puissante :
\pdfextension literal direct { ... }
(Note : pdfTex aussi mais avec la syntaxe \pdfliteral, voir l'annexe.)
Cette commande permet d'injecter directement du code PDF brut dans le flux de sortie, sans aucune interprétation supplémentaire par TeX. Le contenu est transmis tel quel au moteur PDF. C'est dangereux (aucune vérification), mais incroyablement rapide par rapport à TikZ.
La liste complÚte des commandes est disponible dans la spécification officielle PDF, voir par exemple la version pour PDF1.7 sur la Wayback Machine. (Pages 226 et suivantes.)
ATTENTION, il est (fin 2025) déconseillé d'essayer de "vibe-coder" la partie qui suit : il existe trÚs peu de contenu à ce propos sur le net, les modÚles de langage sont trÚs mal entraßnés et inventent en permanence la syntaxe et se contredisent d'une réponse à l'autre, ce qui peut se révéler assez frustrant. RTFM.
Voici les primitives que l'on utilisera pour cet exemple :
- Couleur (valeur RGB de 0 Ă 1, ici du noir) :
0 0 0 rg - Rectangle :
x y largeur hauteur re - Remplissage (fill) :
f
Par exemple, pour dessiner un rectangle noir de 2 pt par 1 pt en bas Ă gauche de la page (Ă dix points de l'angle) :
\pdfextension literal direct { 0 0 0 rg 10 10 2 1 re f }
Aucun package graphique, aucune macro : le PDF est écrit quasiment à la main.
Remarque : en rÚgle générale, lorsque l'on insÚre des commandes dans le flux pdf, il vaut mieux commencer par sauvegarder l'état graphique (avec q), puis le restaurer à la fin des instructions (avec Q):
\pdfextension literal direct { q 0 0 0 rg 10 10 2 1 re f Q }
On peut aussi faire beaucoup d'autres choses : courbes de Bézier etc, avec une syntaxe qui n'est pas si compliquée que cela.
Stratégie générale pour les triplets pythagoriciens
Notre objectif reste le mĂȘme que dans l'article prĂ©cĂ©dent : parcourir tous les couples d'entiers $(x,y)$ infĂ©rieurs Ă une borne, vĂ©rifier s'ils constituent le dĂ©but d'un triplet pythagoricien, et si oui, dessiner un pixel noir aux coordonnĂ©es $(x,y)$.
Mais cette fois, les instructions de dessin sont des primitives PDF au lieu de commandes TikZ. Elles sont écrites en une seule fois à la fin du code à l'aide d'une seule instruction \pdfextension literal direct{ ... }.
Calcul efficace des carrés parfaits
Comme précédemment, on pré-calcule tous les carrés :
local squares = {}
local max = math.floor(math.sqrt(2*N*N))
for i = 1, max do
squares[i*i] = true
end
Cela permet de tester trÚs rapidement si un nombre n est un carré parfait avec if squares[n].
Gestion des unités et de l'échelle
Le PDF travaille en points typographiques (pt). Un point est un un 72Úme de pouce. Tout ceci est absurde et on ne veut surtout pas en entendre parler, donc on introduit donc quelques conversions pour préciser la taille des pixels en centimÚtres.
local CM_PER_PIXEL = .005
local PT_PER_PIXEL= CM_PER_PIXEL * 72 / 2.54
Ici, la taille des pixels est de 0,5 millimÚtres. (Un facteur de grossissement sera ensuite appliqué sur chaque pixel pour mieux les distinguer.)
Génération massive des rectangles
On stocke toutes les commandes PDF dans une table Lua (à la place des commandes TikZ dans la version précédente), en exploitant la symétrie d'axe $y=x$.
Les commandes seront écrites d'un coup par la suite, pour ne pas écrire de façon répétée du TeX, et aussi pour séparer la partie algorithmique (test des triplets) de la partie purement d'écriture.
local function draw_pixel(x,y)
local scale = 4 -- grossissement des pixels
local px = x * PT_PER_PIXEL
local py = y * PT_PER_PIXEL
return string.format("%.2f %.2f %.2f %.2f re f",
px, py, PT_PER_PIXEL*scale, PT_PER_PIXEL*scale)
end
local out = {}
for x = 1, N do
for y = 1, x do
local s = x*x + y*y
if squares[s] then
table.insert(out,draw_pixel(x,y))
table.insert(out,draw_pixel(y,x))
end
end
end
Ă ce stade, des milliers de rectangles PDF sont prĂȘts, mais encore stockĂ©s en mĂ©moire Lua dans la table out.
Injection finale des primitives PDF dans la source LaTeX
On écrit les commandes PDF en une seule fois, à la fin :
tex.sprint("\\pdfextension literal direct {")
tex.sprint(table.concat(out, "\n"))
tex.sprint("}")
Mesure des performances
Grùce à os.clock(), on peut mesurer précisément les différentes phases : calcul des triplets, génération des commandes PDF, écriture finale. Résultat : environ 13 secondes avec TikZ, contre environ 10 millisecondes (!) avec l'écriture pdf directe.
Autrement dit, il s'agit d'une amélioration d'un facteur 1000. On remarque aussi que le fichier pdf est dix fois plus léger que celui produit avec TikZ.
Le code complet
\documentclass{article}
\usepackage[paperwidth=20cm, paperheight=20cm]{geometry}
\usepackage{luacode}
\begin{document}
\pagestyle{empty}
\leavevmode
\begin{luacode*}
local CM_PER_PIXEL = .005
local PT_PER_PIXEL= CM_PER_PIXEL * 72 / 2.54
local N = 4000
local squares = {}
local max = math.floor(math.sqrt(2*N*N))
for i = 1, max do
squares[i*i] = true
end
local function draw_pixel(x,y)
local scale = 4 -- grossissement des pixels
local px = x * PT_PER_PIXEL
local py = y * PT_PER_PIXEL
return string.format("%.2f %.2f %.2f %.2f re f",
px, py, PT_PER_PIXEL*scale, PT_PER_PIXEL*scale)
end
local out = {}
for x = 1, N do
for y = 1, x do
local s = x*x + y*y
if squares[s] then
table.insert(out,draw_pixel(x,y))
table.insert(out,draw_pixel(y,x))
end
end
end
tex.sprint("\\pdfextension literal direct {0 0 0 rg ")
tex.sprint(table.concat(out, "\n"))
tex.sprint("}")
\end{luacode*}
\end{document}
PiĂšges classiques :
- ne pas utiliser standalone avec les commandes pdf.
- ne pas oublier le
\leavevmode
Conclusion
TikZ reste indispensable pour les figures complexes et lisibles. Mais pour le rendu massif de données, les primitives PDF sont imbattables.
Annexe : primitives PDF en utilisant le moteur pdftex au lieu de Luatex
Sous pdflatex, la commande serait :
\pdfliteral direct { 0 0 0 rg 10 10 1 1 re f }
On pourrait mĂȘme imaginer une abstraction portable entre pdftex et luatex :
\newcommand\PDFLiteral[1]{%
\ifdefined\pdfextension
\pdfextension literal direct {#1}%
\else
\pdfliteral direct {#1}%
\fi
}
Mais de toute façon, sans Lua pour les boucles et les calculs, ça n'a pas tellement d'intĂ©rĂȘt d'utiliser pdftex.