-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathlatex2svg.py
229 lines (194 loc) · 6.95 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/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
from PIL import Image
default_template = r"""
\documentclass[preview,fontsize={{ fontsize }}pt]{standalone}
{{ preamble }}
{{ macros }}
\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}
"""
default_macros = r"""
"""
latex_cmd = 'latex -interaction nonstopmode -halt-on-error'
dvisvgm_cmd = 'dvisvgm --no-fonts'
dvipng_cmd = 'dvipng -D 250 -bg Transparent --width --height '
default_params = {
'fontsize': 14, # pt
'template': default_template,
'preamble': default_preamble,
'macros': default_macros,
'latex_cmd': latex_cmd,
'dvisvgm_cmd': dvisvgm_cmd,
'dvipng_cmd': dvipng_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 latex2dvi(code, working_directory, params=default_params):
fontsize = params['fontsize']
document = (params['template']
.replace('{{ preamble }}', params['preamble'])
.replace('{{ macros }}', params['macros'])
.replace('{{ fontsize }}', str(fontsize))
.replace('{{ code }}', code))
print(document)
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')
def latex2png(code, params=default_params, working_directory=None):
if working_directory is None:
with TemporaryDirectory() as tmpdir:
return latex2png(code, params, working_directory=tmpdir)
latex2dvi(code, working_directory, params)
return dvi2png(params, working_directory)
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)
latex2dvi(code, working_directory, params)
return dvi2svg(params, working_directory)
def dvi2svg(params, working_directory):
fontsize = params['fontsize']
# 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
def get_size(output):
regex = r'\b([0-9.]+)pt x ([0-9.]+)pt'
match = re.search(regex, output)
if match:
width = round(float(match.group(1)) / 72 * 96, 2)
height = round(float(match.group(2)) / 72 * 96, 2)
return (width, height)
else:
return None, None
output = ret.stderr.decode('utf-8')
width, height = get_size(output)
print(width)
print(height)
return {'svg': svg, 'width': width, 'height': height}
def dvi2png(params, working_directory):
env = os.environ.copy()
try:
ret = subprocess.run(shlex.split(params['dvipng_cmd']+' -o code.png code.dvi'),
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
cwd=working_directory, env=env)
ret.check_returncode()
except FileNotFoundError:
raise RuntimeError('dvipng not found')
with open(os.path.join(working_directory, 'code.png'), 'rb') as f:
png = f.read()
output = ret.stdout.decode('utf-8')
print(output)
search = re.search(r'height=(\d+) width=(\d+)', output)
if len(search.groups()) != 2:
raise Exception('size wrong')
height = search.group(1)
width = search.group(2)
print(height)
print(width)
return {'png': png, '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()