"""tikz.py — rend des blocs tikzpicture en SVG (pdflatex standalone → pdftocairo), avec cache.""" from __future__ import annotations import hashlib import os import re import shutil import subprocess import sys import tempfile from concurrent.futures import ThreadPoolExecutor PREAMBLE = r""" \documentclass[tikz,border=5pt]{standalone} \usepackage[utf8]{inputenc} \usepackage[T1]{fontenc} \usepackage[french]{babel} \usepackage{lmodern} \usepackage[sfdefault,scaled=0.94]{FiraSans} \usepackage{amsmath,amssymb} \usepackage[table]{xcolor} \usepackage{tikz} \usepackage{pgfplots} \usepackage{fontawesome5} \usepackage{pifont} \usepackage{textcomp} \usepackage[locale=FR, group-separator={\,}, group-minimum-digits=4]{siunitx} \pgfplotsset{compat=1.18} \usetikzlibrary{shapes,arrows,arrows.meta,positioning,calc,decorations.pathreplacing,shadows,fit,babel,patterns,backgrounds,matrix,chains,shapes.geometric,shapes.misc} \definecolor{uqoBleu}{RGB}{0, 62, 126} \definecolor{uqoBleuClair}{RGB}{41, 118, 187} \definecolor{uqoOr}{RGB}{204, 164, 36} \definecolor{uqoGris}{RGB}{96, 102, 112} \definecolor{uqoGrisClair}{RGB}{208, 213, 220} \definecolor{uqoBlanc}{RGB}{255, 255, 255} \definecolor{uqoVert}{RGB}{16, 124, 78} \definecolor{uqoRouge}{RGB}{178, 42, 32} \definecolor{uqoBgClair}{RGB}{242, 246, 250} \definecolor{uqoNavy}{RGB}{9, 26, 51} \definecolor{uqoInk}{RGB}{38, 43, 52} \color{uqoInk} \newcommand{\uqotitle}[1]{\textcolor{uqoBleu}{\textbf{#1}}} \newcommand{\uqohighlight}[1]{\textcolor{uqoOr!75!uqoGris}{\textbf{#1}}} \newcommand{\uqoalert}[1]{\textcolor{uqoRouge}{\textbf{#1}}} \newcommand{\uqogreen}[1]{\textcolor{uqoVert}{\textbf{#1}}} \newcommand{\motcle}[1]{\textcolor{uqoBleu}{\textbf{#1}}} \newcommand{\dollars}[1]{\num{#1}\,\$} \newcommand{\repere}[1]{} \newcommand{\margterme}[2]{} \tikzset{ uqoflow/.style={rectangle, rounded corners, draw=uqoBleu, fill=uqoBgClair, text centered, font=\small, minimum height=0.6cm}, uqostep/.style={uqoflow, text width=10.5cm}, uqocard/.style={rectangle, rounded corners=6pt, draw=uqoBleu, fill=uqoBgClair, text centered, font=\small}, uqoarrow/.style={->, thick, uqoBleu}, } \pgfplotsset{ /pgf/number format/1000 sep={\,}, every axis/.append style={ axis line style={uqoGris!55}, tick style={uqoGris!55}, tick label style={font=\scriptsize, color=uqoGris}, label style={font=\small, color=uqoInk}, legend style={font=\scriptsize, draw=uqoGrisClair, fill=white, fill opacity=0.9}, }, } \begin{document} """ POSTAMBLE = "\n\\end{document}\n" def tikz_hash(tex: str) -> str: return hashlib.sha1((PREAMBLE + tex).encode("utf-8")).hexdigest()[:16] def render_one(tex: str, out_svg: str, log_dir: str | None = None) -> tuple[bool, str]: tmp = tempfile.mkdtemp(prefix="tikz-") try: src = os.path.join(tmp, "fig.tex") with open(src, "w", encoding="utf-8") as f: f.write(PREAMBLE + tex + POSTAMBLE) for _ in range(1): p = subprocess.run(["pdflatex", "-interaction=nonstopmode", "-halt-on-error", "fig.tex"], cwd=tmp, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180) pdf = os.path.join(tmp, "fig.pdf") if not os.path.exists(pdf): log = p.stdout.decode("utf-8", "replace") if log_dir: os.makedirs(log_dir, exist_ok=True) shutil.copy(src, os.path.join(log_dir, os.path.basename(out_svg) + ".tex")) with open(os.path.join(log_dir, os.path.basename(out_svg) + ".log"), "w") as f: f.write(log) err = re.findall(r"^! .*$", log, flags=re.M) return False, "; ".join(err[:3]) or log[-500:] svg = os.path.join(tmp, "fig.svg") q = subprocess.run(["pdftocairo", "-svg", pdf, svg], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120) if not os.path.exists(svg): return False, "pdftocairo : " + q.stdout.decode("utf-8", "replace")[-300:] os.makedirs(os.path.dirname(out_svg), exist_ok=True) shutil.move(svg, out_svg) return True, "" finally: shutil.rmtree(tmp, ignore_errors=True) def svg_size(path: str) -> tuple[float, float]: head = open(path, encoding="utf-8", errors="replace").read(2000) m = re.search(r']*\swidth="([\d.]+)(pt|px)?"[^>]*\sheight="([\d.]+)(pt|px)?"', head) if not m: return 0.0, 0.0 w = float(m.group(1)) h = float(m.group(3)) if m.group(2) == "pt": w *= 4 / 3 h *= 4 / 3 return w, h def render_all(tikz_list: list[str], cache_dir: str, workers: int = 6, log_dir: str | None = None) -> list[dict]: """Retourne, pour chaque bloc, {'hash', 'path', 'ok', 'err', 'w', 'h'}.""" os.makedirs(cache_dir, exist_ok=True) results: list[dict] = [None] * len(tikz_list) # type: ignore todo = [] for idx, tex in enumerate(tikz_list): h = tikz_hash(tex) path = os.path.join(cache_dir, h + ".svg") results[idx] = {"hash": h, "path": path, "ok": os.path.exists(path), "err": "", "w": 0, "h": 0} if not results[idx]["ok"]: todo.append(idx) def work(idx): ok, err = render_one(tikz_list[idx], results[idx]["path"], log_dir) results[idx]["ok"] = ok results[idx]["err"] = err return idx if todo: print(f" TikZ : {len(todo)} figure(s) à rendre ({len(tikz_list) - len(todo)} en cache)", file=sys.stderr) with ThreadPoolExecutor(max_workers=workers) as ex: for i, _ in enumerate(ex.map(work, todo), 1): if i % 10 == 0 or i == len(todo): print(f" {i}/{len(todo)}", file=sys.stderr) for r in results: if r["ok"]: r["w"], r["h"] = svg_size(r["path"]) return results if __name__ == "__main__": tex = open(sys.argv[1]).read() ok, err = render_one(tex, sys.argv[2]) print(ok, err)