# Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html from __future__ import annotations import html import os import re from dataclasses import dataclass from typing import Any import anki import anki.collection from anki import card_rendering_pb2, hooks from anki.models import NotetypeDict from anki.template import TemplateRenderContext, TemplateRenderOutput from anki.utils import call, is_mac, namedtmp, tmpdir pngCommands = [ ["latex", "-interaction=nonstopmode", "tmp.tex"], ["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"], ] svgCommands = [ ["latex", "-interaction=nonstopmode", "tmp.tex"], ["dvisvgm", "--no-fonts", "--exact", "-Z", "2", "tmp.dvi", "-o", "tmp.svg"], ] # if off, use existing media but don't create new build = True # pylint: disable=invalid-name # add standard tex install location to osx if is_mac: os.environ["PATH"] += ":/usr/texbin:/Library/TeX/texbin" @dataclass class ExtractedLatex: filename: str latex_body: str @dataclass class ExtractedLatexOutput: html: str latex: list[ExtractedLatex] @staticmethod def from_proto( proto: card_rendering_pb2.ExtractLatexResponse, ) -> ExtractedLatexOutput: return ExtractedLatexOutput( html=proto.text, latex=[ ExtractedLatex(filename=l.filename, latex_body=l.latex_body) for l in proto.latex ], ) def on_card_did_render( output: TemplateRenderOutput, ctx: TemplateRenderContext ) -> None: output.question_text = render_latex( output.question_text, ctx.note_type(), ctx.col() ) output.answer_text = render_latex(output.answer_text, ctx.note_type(), ctx.col()) def render_latex( html: str, model: NotetypeDict, col: anki.collection.Collection ) -> str: "Convert embedded latex tags in text to image links." html, err = render_latex_returning_errors(html, model, col) if err: html += "\n".join(err) return html def render_latex_returning_errors( html: str, model: NotetypeDict, col: anki.collection.Collection, expand_clozes: bool = False, ) -> tuple[str, list[str]]: """Returns (text, errors). errors will be non-empty if LaTeX failed to render.""" svg = model.get("latexsvg", False) header = model["latexPre"] footer = model["latexPost"] proto = col._backend.extract_latex(text=html, svg=svg, expand_clozes=expand_clozes) out = ExtractedLatexOutput.from_proto(proto) errors = [] html = out.html for latex in out.latex: # don't need to render? if not build or col.media.have(latex.filename): continue err = _save_latex_image(col, latex, header, footer, svg) if err is not None: errors.append(err) return html, errors def _save_latex_image( col: anki.collection.Collection, extracted: ExtractedLatex, header: str, footer: str, svg: bool, ) -> str | None: # add header/footer latex = f"{header}\n{extracted.latex_body}\n{footer}" # it's only really secure if run in a jail, but these are the most common tmplatex = latex.replace("\\includegraphics", "") for bad in ( "\\write18", "\\readline", "\\input", "\\include", "\\catcode", "\\openout", "\\write", "\\loop", "\\def", "\\shipout", ): # don't mind if the sequence is only part of a command bad_re = f"\\{bad}[^a-zA-Z]" if re.search(bad_re, tmplatex): return col.tr.media_for_security_reasons_is_not(val=bad) # commands to use if svg: latex_cmds = svgCommands ext = "svg" else: latex_cmds = pngCommands ext = "png" # write into a temp file log = open(namedtmp("latex_log.txt"), "w", encoding="utf8") texpath = namedtmp("tmp.tex") texfile = open(texpath, "w", encoding="utf8") texfile.write(latex) texfile.close() oldcwd = os.getcwd() png_or_svg = namedtmp(f"tmp.{ext}") try: # generate png/svg os.chdir(tmpdir()) for latex_cmd in latex_cmds: if call(latex_cmd, stdout=log, stderr=log): return _err_msg(col, latex_cmd[0], texpath) # add to media with open(png_or_svg, "rb") as file: data = file.read() col.media.write_data(extracted.filename, data) os.unlink(png_or_svg) return None finally: os.chdir(oldcwd) log.close() def _err_msg(col: anki.collection.Collection, type: str, texpath: str) -> Any: msg = f"{col.tr.media_error_executing(val=type)}
" msg += f"{col.tr.media_generated_file(val=texpath)}
" try: with open(namedtmp("latex_log.txt", remove=False), encoding="utf8") as file: log = file.read() if not log: raise Exception() msg += f"
{html.escape(log)}
" except: msg += col.tr.media_have_you_installed_latex_and_dvipngdvisvgm() return msg def setup_hook() -> None: hooks.card_did_render.append(on_card_did_render)