#!/usr/bin/env python3 """latex2svg -- This version of latex2svg comes with NML and has been modified to work with it only -- -- The original version can be found here : https://github.com/Moonbase59/latex2svg -- Read LaTeX code from stdin and render a SVG using LaTeX, dvisvgm and svgo. Returns a minified SVG with `width`, `height` and `style="vertical-align:"` attribues whose values are in `em` units. The SVG will have (pseudo-)unique IDs in case more than one is used on the same HTML page. Based on [original work](https://github.com/tuxu/latex2svg) by Tino Wagner. """ __version__ = '0.2.1' __author__ = 'Matthias C. Hormann' __email__ = 'mhormann@gmx.de' __license__ = 'MIT' __copyright__ = 'Copyright (c) 2022, Matthias C. Hormann' import os import sys import subprocess import shlex import re from tempfile import TemporaryDirectory from ctypes.util import find_library default_svgo_config = r""" module.exports = { plugins: [ { // use default preset (almost) name: 'preset-default', params: { overrides: { // viewbox required to resize SVGs with CSS, disable removal removeViewBox: false, }, }, // enable prefixIds name: 'prefixIds', params: { prefix: '{{ prefix }}', delim: '_', }, }, ], }; """ latex_cmd = 'latex -interaction nonstopmode -halt-on-error' dvisvgm_cmd = 'dvisvgm --no-fonts' svgo_cmd = 'svgo' default_params = { 'fontsize': 12, # TeX pt 'latex_cmd': latex_cmd, 'dvisvgm_cmd': dvisvgm_cmd, 'svgo_cmd': svgo_cmd, 'svgo_config': default_svgo_config, 'libgs': None, } if not hasattr(os.environ, 'LIBGS') and not find_library('gs'): if sys.platform == 'darwin': # Fallback to homebrew Ghostscript on macOS homebrew_libgs = '/usr/local/opt/ghostscript/lib/libgs.dylib' if os.path.exists(homebrew_libgs): default_params['libgs'] = homebrew_libgs if not default_params['libgs']: print('Warning: libgs not found', file=sys.stderr) def latex2svg(code, params=default_params, working_directory=None): """Convert LaTeX to SVG using dvisvgm and svgo. Parameters ---------- code : str LaTeX code to render. params : dict Conversion parameters. working_directory : str or None Working directory for external commands and place for temporary files. Returns ------- dict Dictionary of SVG output and output information: * `svg`: SVG data * `width`: image width in *em* * `height`: image height in *em* * `valign`: baseline offset in *em* """ if working_directory is None: with TemporaryDirectory() as tmpdir: return latex2svg(code, params, working_directory=tmpdir) # Caution: TeX & dvisvgm work with TeX pt (1/72.27"), but we need DTP pt (1/72") # so we need a scaling factor for correct output sizes scaling = 1.00375 # (1/72)/(1/72.27) fontsize = params['fontsize'] document = code with open(os.path.join(working_directory, 'code.tex'), 'w') as f: f.write(document) # Run LaTeX and create DVI file try: ret = subprocess.run(shlex.split(params['latex_cmd']+' code.tex'), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory) ret.check_returncode() except FileNotFoundError: raise RuntimeError('latex not found') # Add LIBGS to environment if supplied env = os.environ.copy() if params['libgs']: env['LIBGS'] = params['libgs'] # Convert DVI to SVG try: ret = subprocess.run(shlex.split(params['dvisvgm_cmd']+' code.dvi'), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory, env=env) ret.check_returncode() except FileNotFoundError: raise RuntimeError('dvisvgm not found') # Parse dvisvgm output for size and alignment def get_size(output): regex = r'\b([0-9.]+)pt x ([0-9.]+)pt' match = re.search(regex, output) if match: return (float(match.group(1)) / fontsize * scaling, float(match.group(2)) / fontsize * scaling) else: return None, None def get_measure(output, name): regex = r'\b%s=([0-9.e-]+)pt' % name match = re.search(regex, output) if match: return float(match.group(1)) / fontsize * scaling else: return None output = ret.stderr.decode('utf-8') width, height = get_size(output) depth = get_measure(output, 'depth') # Modify SVG attributes, to a get a self-contained, scaling SVG from lxml import etree # read SVG, discarding all comments ("<-- Generated by… -->") parser = etree.XMLParser(remove_comments=True) xml = etree.parse(os.path.join(working_directory, 'code.svg'), parser) svg = xml.getroot() svg.set('width', f'{width:.6f}em') svg.set('height', f'{height:.6f}em') svg.set('style', f'vertical-align:{-depth:.6f}em') xml.write(os.path.join(working_directory, 'code.svg')) # Run svgo to get a minified oneliner with (pseudo-)unique Ids # generate random prefix using ASCII letters (ID may not start with a digit) import random, string prefix = ''.join(random.choice(string.ascii_letters) for n in range(4)) svgo_config = (params['svgo_config'] .replace('{{ prefix }}', prefix)) # write svgo params file with open(os.path.join(working_directory, 'svgo.config.js'), 'w') as f: f.write(svgo_config) try: ret = subprocess.run(shlex.split(params['svgo_cmd']+' code.svg'), stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_directory, env=env) ret.check_returncode() except FileNotFoundError: raise RuntimeError('svgo not found') with open(os.path.join(working_directory, 'code.svg'), 'r') as f: svg = f.read() return {'svg': svg, 'valign': round(-depth,6), 'width': round(width,6), 'height': round(height,6)} def main(): """Simple command line interface to latex2svg. - Read from `stdin`. - Write SVG to `stdout`. - On error: write error messages to `stderr` and return with error code. """ import json import argparse parser = argparse.ArgumentParser(description=""" Render LaTeX code from stdin as SVG to stdout. Writes metadata (baseline offset, width, height in em units) into the SVG attributes. """) parser.add_argument('--version', action='version', version='%(prog)s {version}'.format(version=__version__)) parser.add_argument('--fontsize', help="LaTeX fontsize in pt") args = parser.parse_args() fontsize = 12 if args.fontsize is not None: fontsize = int(args.fontsize) latex = sys.stdin.read() try: params = default_params.copy() params['fontsize'] = fontsize out = latex2svg(latex, params) sys.stdout.write(out['svg']) except subprocess.CalledProcessError as exc: # LaTeX prints errors on stdout instead of stderr (stderr is empty), # dvisvgm to stderr, so print both (to stderr) print(exc.output.decode('utf-8'), file=sys.stderr) print(exc.stderr.decode('utf-8'), file=sys.stderr) sys.exit(exc.returncode) if __name__ == '__main__': main()