-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathlatex2svg.py
executable file
·185 lines (158 loc) · 5.46 KB
/
latex2svg.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/env python3
"""latex2svg
Read LaTeX code from stdin and render a SVG using LaTeX + dvisvgm.
"""
__version__ = '0.1.0'
__author__ = 'Tino Wagner'
__email__ = '[email protected]'
__license__ = 'MIT'
__copyright__ = '(c) 2017, Tino Wagner'
import os
import sys
import subprocess
import shlex
import re
from tempfile import TemporaryDirectory
from ctypes.util import find_library
default_template = r"""
\documentclass[{{ fontsize }}pt,preview]{standalone}
{{ preamble }}
\begin{document}
\begin{preview}
{{ code }}
\end{preview}
\end{document}
"""
default_preamble = r"""
\usepackage[utf8x]{inputenc}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{newtxtext}
\usepackage[libertine]{newtxmath}
"""
latex_cmd = 'latex -interaction nonstopmode -halt-on-error'
dvisvgm_cmd = 'dvisvgm --no-fonts'
default_params = {
'fontsize': 12, # pt
'template': default_template,
'preamble': default_preamble,
'latex_cmd': latex_cmd,
'dvisvgm_cmd': dvisvgm_cmd,
'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')
def latex2svg(code, params=default_params, working_directory=None):
"""Convert LaTeX to SVG using dvisvgm.
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*
* `depth`: baseline position in *em*
"""
if working_directory is None:
with TemporaryDirectory() as tmpdir:
return latex2svg(code, params, working_directory=tmpdir)
fontsize = params['fontsize']
document = (params['template']
.replace('{{ preamble }}', params['preamble'])
.replace('{{ fontsize }}', str(fontsize))
.replace('{{ code }}', 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')
with open(os.path.join(working_directory, 'code.svg'), 'r') as f:
svg = f.read()
# 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,
float(match.group(2)) / fontsize)
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
else:
return None
output = ret.stderr.decode('utf-8')
width, height = get_size(output)
depth = get_measure(output, 'depth')
return {'svg': svg, 'depth': depth, 'width': width, 'height': height}
def main():
"""Simple command line interface to latex2svg.
- Read from `stdin`.
- Write SVG to `stdout`.
- Write metadata as JSON to `stderr`.
- On error: write error messages to `stdout` 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
position, width, height in em units) as JSON to stderr.
""")
parser.add_argument('--preamble',
help="LaTeX preamble code to read from file")
args = parser.parse_args()
preamble = default_preamble
if args.preamble is not None:
with open(args.preamble) as f:
preamble = f.read()
latex = sys.stdin.read()
try:
params = default_params.copy()
params['preamble'] = preamble
out = latex2svg(latex, params)
sys.stdout.write(out['svg'])
meta = {key: out[key] for key in out if key != 'svg'}
sys.stderr.write(json.dumps(meta))
except subprocess.CalledProcessError as exc:
# LaTeX prints errors on stdout instead of stderr (stderr is empty),
# dvisvgm to stderr, so print both
print(exc.output.decode('utf-8'))
print(exc.stderr.decode('utf-8'))
sys.exit(exc.returncode)
if __name__ == '__main__':
main()