Skip to content

Commit c51a88d

Browse files
Fix performance regression for imgmath embedding (#10888)
Co-authored-by: Adam Turner <[email protected]>
1 parent 6ed4bbb commit c51a88d

File tree

1 file changed

+58
-61
lines changed

1 file changed

+58
-61
lines changed

sphinx/ext/imgmath.py

+58-61
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Render math in HTML via dvipng or dvisvgm."""
22

33
import base64
4-
import posixpath
54
import re
65
import shutil
76
import subprocess
@@ -157,13 +156,10 @@ def convert_dvi_to_image(command: List[str], name: str) -> Tuple[str, str]:
157156
raise MathExtError('%s exited with error' % name, exc.stderr, exc.stdout) from exc
158157

159158

160-
def convert_dvi_to_png(dvipath: str, builder: Builder) -> Tuple[str, Optional[int]]:
159+
def convert_dvi_to_png(dvipath: str, builder: Builder, out_path: str) -> Optional[int]:
161160
"""Convert DVI file to PNG image."""
162-
tempdir = ensure_tempdir(builder)
163-
filename = path.join(tempdir, 'math.png')
164-
165161
name = 'dvipng'
166-
command = [builder.config.imgmath_dvipng, '-o', filename, '-T', 'tight', '-z9']
162+
command = [builder.config.imgmath_dvipng, '-o', out_path, '-T', 'tight', '-z9']
167163
command.extend(builder.config.imgmath_dvipng_args)
168164
if builder.config.imgmath_use_preview:
169165
command.append('--depth')
@@ -177,19 +173,16 @@ def convert_dvi_to_png(dvipath: str, builder: Builder) -> Tuple[str, Optional[in
177173
matched = depth_re.match(line)
178174
if matched:
179175
depth = int(matched.group(1))
180-
write_png_depth(filename, depth)
176+
write_png_depth(out_path, depth)
181177
break
182178

183-
return filename, depth
179+
return depth
184180

185181

186-
def convert_dvi_to_svg(dvipath: str, builder: Builder) -> Tuple[str, Optional[int]]:
182+
def convert_dvi_to_svg(dvipath: str, builder: Builder, out_path: str) -> Optional[int]:
187183
"""Convert DVI file to SVG image."""
188-
tempdir = ensure_tempdir(builder)
189-
filename = path.join(tempdir, 'math.svg')
190-
191184
name = 'dvisvgm'
192-
command = [builder.config.imgmath_dvisvgm, '-o', filename]
185+
command = [builder.config.imgmath_dvisvgm, '-o', out_path]
193186
command.extend(builder.config.imgmath_dvisvgm_args)
194187
command.append(dvipath)
195188

@@ -201,16 +194,16 @@ def convert_dvi_to_svg(dvipath: str, builder: Builder) -> Tuple[str, Optional[in
201194
matched = depthsvg_re.match(line)
202195
if matched:
203196
depth = round(float(matched.group(1)) * 100 / 72.27) # assume 100ppi
204-
write_svg_depth(filename, depth)
197+
write_svg_depth(out_path, depth)
205198
break
206199

207-
return filename, depth
200+
return depth
208201

209202

210203
def render_math(
211204
self: HTMLTranslator,
212205
math: str,
213-
) -> Tuple[Optional[str], Optional[int], Optional[str], Optional[str]]:
206+
) -> Tuple[Optional[str], Optional[int]]:
214207
"""Render the LaTeX math expression *math* using latex and dvipng or
215208
dvisvgm.
216209
@@ -234,43 +227,43 @@ def render_math(
234227
self.builder.config,
235228
self.builder.confdir)
236229

237-
filename = "%s.%s" % (sha1(latex.encode()).hexdigest(), image_format)
238-
relfn = posixpath.join(self.builder.imgpath, 'math', filename)
239-
outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', filename)
240-
if path.isfile(outfn):
230+
filename = f"{sha1(latex.encode()).hexdigest()}.{image_format}"
231+
generated_path = path.join(self.builder.outdir, self.builder.imagedir, 'math', filename)
232+
ensuredir(path.dirname(generated_path))
233+
if path.isfile(generated_path):
241234
if image_format == 'png':
242-
depth = read_png_depth(outfn)
235+
depth = read_png_depth(generated_path)
243236
elif image_format == 'svg':
244-
depth = read_svg_depth(outfn)
245-
return relfn, depth, None, outfn
237+
depth = read_svg_depth(generated_path)
238+
return generated_path, depth
246239

247240
# if latex or dvipng (dvisvgm) has failed once, don't bother to try again
248241
if hasattr(self.builder, '_imgmath_warned_latex') or \
249242
hasattr(self.builder, '_imgmath_warned_image_translator'):
250-
return None, None, None, None
243+
return None, None
251244

252245
# .tex -> .dvi
253246
try:
254247
dvipath = compile_math(latex, self.builder)
255248
except InvokeError:
256249
self.builder._imgmath_warned_latex = True # type: ignore
257-
return None, None, None, None
250+
return None, None
258251

259252
# .dvi -> .png/.svg
260253
try:
261254
if image_format == 'png':
262-
imgpath, depth = convert_dvi_to_png(dvipath, self.builder)
255+
depth = convert_dvi_to_png(dvipath, self.builder, generated_path)
263256
elif image_format == 'svg':
264-
imgpath, depth = convert_dvi_to_svg(dvipath, self.builder)
257+
depth = convert_dvi_to_svg(dvipath, self.builder, generated_path)
265258
except InvokeError:
266259
self.builder._imgmath_warned_image_translator = True # type: ignore
267-
return None, None, None, None
260+
return None, None
268261

269-
return relfn, depth, imgpath, outfn
262+
return generated_path, depth
270263

271264

272-
def render_maths_to_base64(image_format: str, outfn: Optional[str]) -> str:
273-
with open(outfn, "rb") as f:
265+
def render_maths_to_base64(image_format: str, generated_path: Optional[str]) -> str:
266+
with open(generated_path, "rb") as f:
274267
encoded = base64.b64encode(f.read()).decode(encoding='utf-8')
275268
if image_format == 'png':
276269
return f'data:image/png;base64,{encoded}'
@@ -279,15 +272,23 @@ def render_maths_to_base64(image_format: str, outfn: Optional[str]) -> str:
279272
raise MathExtError('imgmath_image_format must be either "png" or "svg"')
280273

281274

282-
def cleanup_tempdir(app: Sphinx, exc: Exception) -> None:
275+
def clean_up_files(app: Sphinx, exc: Exception) -> None:
283276
if exc:
284277
return
285-
if not hasattr(app.builder, '_imgmath_tempdir'):
286-
return
287-
try:
288-
shutil.rmtree(app.builder._imgmath_tempdir) # type: ignore
289-
except Exception:
290-
pass
278+
279+
if hasattr(app.builder, '_imgmath_tempdir'):
280+
try:
281+
shutil.rmtree(app.builder._imgmath_tempdir) # type: ignore
282+
except Exception:
283+
pass
284+
285+
if app.builder.config.imgmath_embed:
286+
# in embed mode, the images are still generated in the math output dir
287+
# to be shared across workers, but are not useful to the final document
288+
try:
289+
shutil.rmtree(path.join(app.builder.outdir, app.builder.imagedir, 'math'))
290+
except Exception:
291+
pass
291292

292293

293294
def get_tooltip(self: HTMLTranslator, node: Element) -> str:
@@ -298,28 +299,26 @@ def get_tooltip(self: HTMLTranslator, node: Element) -> str:
298299

299300
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
300301
try:
301-
fname, depth, imgpath, outfn = render_math(self, '$' + node.astext() + '$')
302+
rendered_path, depth = render_math(self, '$' + node.astext() + '$')
302303
except MathExtError as exc:
303304
msg = str(exc)
304305
sm = nodes.system_message(msg, type='WARNING', level=2,
305306
backrefs=[], source=node.astext())
306307
sm.walkabout(self)
307308
logger.warning(__('display latex %r: %s'), node.astext(), msg)
308309
raise nodes.SkipNode from exc
309-
if self.builder.config.imgmath_embed:
310-
image_format = self.builder.config.imgmath_image_format.lower()
311-
img_src = render_maths_to_base64(image_format, imgpath)
312-
else:
313-
# Move generated image on tempdir to build dir
314-
if imgpath is not None:
315-
ensuredir(path.dirname(outfn))
316-
shutil.move(imgpath, outfn)
317-
img_src = fname
318-
if img_src is None:
310+
311+
if rendered_path is None:
319312
# something failed -- use text-only as a bad substitute
320313
self.body.append('<span class="math">%s</span>' %
321314
self.encode(node.astext()).strip())
322315
else:
316+
if self.builder.config.imgmath_embed:
317+
image_format = self.builder.config.imgmath_image_format.lower()
318+
img_src = render_maths_to_base64(image_format, rendered_path)
319+
else:
320+
relative_path = path.relpath(rendered_path, self.builder.outdir)
321+
img_src = relative_path.replace(path.sep, '/')
323322
c = f'<img class="math" src="{img_src}"' + get_tooltip(self, node)
324323
if depth is not None:
325324
c += f' style="vertical-align: {-depth:d}px"'
@@ -333,7 +332,7 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
333332
else:
334333
latex = wrap_displaymath(node.astext(), None, False)
335334
try:
336-
fname, depth, imgpath, outfn = render_math(self, latex)
335+
rendered_path, depth = render_math(self, latex)
337336
except MathExtError as exc:
338337
msg = str(exc)
339338
sm = nodes.system_message(msg, type='WARNING', level=2,
@@ -348,20 +347,18 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
348347
self.body.append('<span class="eqno">(%s)' % number)
349348
self.add_permalink_ref(node, _('Permalink to this equation'))
350349
self.body.append('</span>')
351-
if self.builder.config.imgmath_embed:
352-
image_format = self.builder.config.imgmath_image_format.lower()
353-
img_src = render_maths_to_base64(image_format, imgpath)
354-
else:
355-
# Move generated image on tempdir to build dir
356-
if imgpath is not None:
357-
ensuredir(path.dirname(outfn))
358-
shutil.move(imgpath, outfn)
359-
img_src = fname
360-
if img_src is None:
350+
351+
if rendered_path is None:
361352
# something failed -- use text-only as a bad substitute
362353
self.body.append('<span class="math">%s</span></p>\n</div>' %
363354
self.encode(node.astext()).strip())
364355
else:
356+
if self.builder.config.imgmath_embed:
357+
image_format = self.builder.config.imgmath_image_format.lower()
358+
img_src = render_maths_to_base64(image_format, rendered_path)
359+
else:
360+
relative_path = path.relpath(rendered_path, self.builder.outdir)
361+
img_src = relative_path.replace(path.sep, '/')
365362
self.body.append(f'<img src="{img_src}"' + get_tooltip(self, node) +
366363
'/></p>\n</div>')
367364
raise nodes.SkipNode
@@ -386,5 +383,5 @@ def setup(app: Sphinx) -> Dict[str, Any]:
386383
app.add_config_value('imgmath_add_tooltips', True, 'html')
387384
app.add_config_value('imgmath_font_size', 12, 'html')
388385
app.add_config_value('imgmath_embed', False, 'html', [bool])
389-
app.connect('build-finished', cleanup_tempdir)
386+
app.connect('build-finished', clean_up_files)
390387
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}

0 commit comments

Comments
 (0)