spb/uqo-imm1033
Public
JavaScript 68%
CSS 32%
1"""tikz.py — rend des blocs tikzpicture en SVG (pdflatex standalone → pdftocairo), avec cache."""2from __future__ import annotations34import hashlib5import os6import re7import shutil8import subprocess9import sys10import tempfile11from concurrent.futures import ThreadPoolExecutor1213PREAMBLE = r"""14\documentclass[tikz,border=5pt]{standalone}15\usepackage[utf8]{inputenc}16\usepackage[T1]{fontenc}17\usepackage[french]{babel}18\usepackage{lmodern}19\usepackage[sfdefault,scaled=0.94]{FiraSans}20\usepackage{amsmath,amssymb}21\usepackage[table]{xcolor}22\usepackage{tikz}23\usepackage{pgfplots}24\usepackage{fontawesome5}25\usepackage{pifont}26\usepackage{textcomp}27\usepackage[locale=FR, group-separator={\,}, group-minimum-digits=4]{siunitx}28\pgfplotsset{compat=1.18}29\usetikzlibrary{shapes,arrows,arrows.meta,positioning,calc,decorations.pathreplacing,shadows,fit,babel,patterns,backgrounds,matrix,chains,shapes.geometric,shapes.misc}30\definecolor{uqoBleu}{RGB}{0, 62, 126}31\definecolor{uqoBleuClair}{RGB}{41, 118, 187}32\definecolor{uqoOr}{RGB}{204, 164, 36}33\definecolor{uqoGris}{RGB}{96, 102, 112}34\definecolor{uqoGrisClair}{RGB}{208, 213, 220}35\definecolor{uqoBlanc}{RGB}{255, 255, 255}36\definecolor{uqoVert}{RGB}{16, 124, 78}37\definecolor{uqoRouge}{RGB}{178, 42, 32}38\definecolor{uqoBgClair}{RGB}{242, 246, 250}39\definecolor{uqoNavy}{RGB}{9, 26, 51}40\definecolor{uqoInk}{RGB}{38, 43, 52}41\color{uqoInk}42\newcommand{\uqotitle}[1]{\textcolor{uqoBleu}{\textbf{#1}}}43\newcommand{\uqohighlight}[1]{\textcolor{uqoOr!75!uqoGris}{\textbf{#1}}}44\newcommand{\uqoalert}[1]{\textcolor{uqoRouge}{\textbf{#1}}}45\newcommand{\uqogreen}[1]{\textcolor{uqoVert}{\textbf{#1}}}46\newcommand{\motcle}[1]{\textcolor{uqoBleu}{\textbf{#1}}}47\newcommand{\dollars}[1]{\num{#1}\,\$}48\newcommand{\repere}[1]{}49\newcommand{\margterme}[2]{}50\tikzset{51 uqoflow/.style={rectangle, rounded corners, draw=uqoBleu, fill=uqoBgClair,52 text centered, font=\small, minimum height=0.6cm},53 uqostep/.style={uqoflow, text width=10.5cm},54 uqocard/.style={rectangle, rounded corners=6pt, draw=uqoBleu, fill=uqoBgClair,55 text centered, font=\small},56 uqoarrow/.style={->, thick, uqoBleu},57}58\pgfplotsset{59 /pgf/number format/1000 sep={\,},60 every axis/.append style={61 axis line style={uqoGris!55},62 tick style={uqoGris!55},63 tick label style={font=\scriptsize, color=uqoGris},64 label style={font=\small, color=uqoInk},65 legend style={font=\scriptsize, draw=uqoGrisClair, fill=white, fill opacity=0.9},66 },67}68\begin{document}69"""7071POSTAMBLE = "\n\\end{document}\n"727374def tikz_hash(tex: str) -> str:75 return hashlib.sha1((PREAMBLE + tex).encode("utf-8")).hexdigest()[:16]767778def render_one(tex: str, out_svg: str, log_dir: str | None = None) -> tuple[bool, str]:79 tmp = tempfile.mkdtemp(prefix="tikz-")80 try:81 src = os.path.join(tmp, "fig.tex")82 with open(src, "w", encoding="utf-8") as f:83 f.write(PREAMBLE + tex + POSTAMBLE)84 for _ in range(1):85 p = subprocess.run(["pdflatex", "-interaction=nonstopmode", "-halt-on-error", "fig.tex"],86 cwd=tmp, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=180)87 pdf = os.path.join(tmp, "fig.pdf")88 if not os.path.exists(pdf):89 log = p.stdout.decode("utf-8", "replace")90 if log_dir:91 os.makedirs(log_dir, exist_ok=True)92 shutil.copy(src, os.path.join(log_dir, os.path.basename(out_svg) + ".tex"))93 with open(os.path.join(log_dir, os.path.basename(out_svg) + ".log"), "w") as f:94 f.write(log)95 err = re.findall(r"^! .*$", log, flags=re.M)96 return False, "; ".join(err[:3]) or log[-500:]97 svg = os.path.join(tmp, "fig.svg")98 q = subprocess.run(["pdftocairo", "-svg", pdf, svg], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120)99 if not os.path.exists(svg):100 return False, "pdftocairo : " + q.stdout.decode("utf-8", "replace")[-300:]101 os.makedirs(os.path.dirname(out_svg), exist_ok=True)102 shutil.move(svg, out_svg)103 return True, ""104 finally:105 shutil.rmtree(tmp, ignore_errors=True)106107108def svg_size(path: str) -> tuple[float, float]:109 head = open(path, encoding="utf-8", errors="replace").read(2000)110 m = re.search(r'<svg[^>]*\swidth="([\d.]+)(pt|px)?"[^>]*\sheight="([\d.]+)(pt|px)?"', head)111 if not m:112 return 0.0, 0.0113 w = float(m.group(1))114 h = float(m.group(3))115 if m.group(2) == "pt":116 w *= 4 / 3117 h *= 4 / 3118 return w, h119120121def render_all(tikz_list: list[str], cache_dir: str, workers: int = 6, log_dir: str | None = None) -> list[dict]:122 """Retourne, pour chaque bloc, {'hash', 'path', 'ok', 'err', 'w', 'h'}."""123 os.makedirs(cache_dir, exist_ok=True)124 results: list[dict] = [None] * len(tikz_list) # type: ignore125 todo = []126 for idx, tex in enumerate(tikz_list):127 h = tikz_hash(tex)128 path = os.path.join(cache_dir, h + ".svg")129 results[idx] = {"hash": h, "path": path, "ok": os.path.exists(path), "err": "", "w": 0, "h": 0}130 if not results[idx]["ok"]:131 todo.append(idx)132133 def work(idx):134 ok, err = render_one(tikz_list[idx], results[idx]["path"], log_dir)135 results[idx]["ok"] = ok136 results[idx]["err"] = err137 return idx138139 if todo:140 print(f" TikZ : {len(todo)} figure(s) à rendre ({len(tikz_list) - len(todo)} en cache)", file=sys.stderr)141 with ThreadPoolExecutor(max_workers=workers) as ex:142 for i, _ in enumerate(ex.map(work, todo), 1):143 if i % 10 == 0 or i == len(todo):144 print(f" {i}/{len(todo)}", file=sys.stderr)145 for r in results:146 if r["ok"]:147 r["w"], r["h"] = svg_size(r["path"])148 return results149150151if __name__ == "__main__":152 tex = open(sys.argv[1]).read()153 ok, err = render_one(tex, sys.argv[2])154 print(ok, err)155