Compile arbitrary LaTeX code to SVG via pdflatex + dvisvgm. latex_code is inserted verbatim inside a standalone document. If preamble_path is a readable file its content is included before \\begin{document}; otherwise amsmath + amssymb are loaded. Returns (svg_bytes, error_tex
(latex_code: str,
preamble_path: str = "")
| 315 | # ── standalone fragment rendering ───────────────────────────────────────────── |
| 316 | |
| 317 | def render_latex_raw(latex_code: str, |
| 318 | preamble_path: str = "") -> tuple[bytes | None, str]: |
| 319 | """ |
| 320 | Compile arbitrary LaTeX code to SVG via pdflatex + dvisvgm. |
| 321 | |
| 322 | latex_code is inserted verbatim inside a standalone document. |
| 323 | If preamble_path is a readable file its content is included before |
| 324 | \\begin{document}; otherwise amsmath + amssymb are loaded. |
| 325 | |
| 326 | Returns (svg_bytes, error_text). On success error_text is "". |
| 327 | Results are cached in CACHE_DIR keyed on content hash. |
| 328 | """ |
| 329 | if not LATEX_AVAILABLE: |
| 330 | return None, "pdflatex or dvisvgm not found" |
| 331 | if preamble_path: |
| 332 | p = Path(preamble_path) |
| 333 | preamble = (p.read_text(encoding="utf-8", errors="replace") |
| 334 | if p.is_file() else |
| 335 | f"% preamble not found: {preamble_path}\n" |
| 336 | r"\usepackage{amsmath}" + "\n" + r"\usepackage{amssymb}" + "\n") |
| 337 | else: |
| 338 | preamble = r"\usepackage{amsmath}" + "\n" + r"\usepackage{amssymb}" + "\n" |
| 339 | |
| 340 | cache_key = hashlib.sha256( |
| 341 | (preamble + "\x00" + latex_code).encode() |
| 342 | ).hexdigest()[:24] |
| 343 | cached = get_cache_dir() / f"frag_{cache_key}.svg" |
| 344 | if cached.exists(): |
| 345 | return cached.read_bytes(), "" |
| 346 | |
| 347 | doc = ( |
| 348 | r"\documentclass[preview,varwidth=true,border=2pt]{standalone}" + "\n" |
| 349 | + preamble + "\n" |
| 350 | + r"\begin{document}" + "\n" |
| 351 | + latex_code + "\n" |
| 352 | + r"\end{document}" + "\n" |
| 353 | ) |
| 354 | with tempfile.TemporaryDirectory(dir=get_cache_dir()) as tmp: |
| 355 | tmpdir = Path(tmp) |
| 356 | tex = tmpdir / "frag.tex" |
| 357 | tex.write_text(doc, encoding="utf-8") |
| 358 | subprocess.run( |
| 359 | ["pdflatex", "-interaction=batchmode", "frag.tex"], |
| 360 | cwd=tmpdir, capture_output=True, |
| 361 | ) |
| 362 | pdf = tmpdir / "frag.pdf" |
| 363 | if not pdf.exists(): |
| 364 | log = tmpdir / "frag.log" |
| 365 | if log.exists(): |
| 366 | lines = log.read_text(encoding="utf-8", errors="replace").splitlines() |
| 367 | errs = [l for l in lines if l.startswith("!")] |
| 368 | return None, "\n".join(errs[:5]) if errs else "pdflatex failed" |
| 369 | return None, "pdflatex failed (no output)" |
| 370 | svg = tmpdir / "frag.svg" |
| 371 | subprocess.run( |
| 372 | ["dvisvgm", "--pdf", "--no-fonts", "frag.pdf", "-o", "frag.svg"], |
| 373 | cwd=tmpdir, capture_output=True, |
| 374 | ) |
no test coverage detected