mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Fix #3835: sphinx.ext.imgmath fails to convert SVG images if project directory name contains spaces
This commit is contained in:
parent
b7b43022cb
commit
32cb60ade3
2
CHANGES
2
CHANGES
@ -22,6 +22,8 @@ Bugs fixed
|
|||||||
* Emit wrong warnings if citation label includes hyphens (refs: #3565)
|
* Emit wrong warnings if citation label includes hyphens (refs: #3565)
|
||||||
* #3858: Some warnings are not colored when using --color option
|
* #3858: Some warnings are not colored when using --color option
|
||||||
* #3775: Remove unwanted whitespace in default template
|
* #3775: Remove unwanted whitespace in default template
|
||||||
|
* #3835: sphinx.ext.imgmath fails to convert SVG images if project directory
|
||||||
|
name contains spaces
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
--------
|
--------
|
||||||
|
@ -52,6 +52,12 @@ class MathExtError(SphinxError):
|
|||||||
SphinxError.__init__(self, msg)
|
SphinxError.__init__(self, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class InvokeError(SphinxError):
|
||||||
|
"""errors on invoking converters."""
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORT_FORMAT = ('png', 'svg')
|
||||||
|
|
||||||
DOC_HEAD = r'''
|
DOC_HEAD = r'''
|
||||||
\documentclass[12pt]{article}
|
\documentclass[12pt]{article}
|
||||||
\usepackage[utf8x]{inputenc}
|
\usepackage[utf8x]{inputenc}
|
||||||
@ -82,6 +88,131 @@ DOC_BODY_PREVIEW = r'''
|
|||||||
depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]')
|
depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]')
|
||||||
|
|
||||||
|
|
||||||
|
def generate_latex_macro(math, config):
|
||||||
|
# type: (unicode, Config) -> unicode
|
||||||
|
"""Generate LaTeX macro."""
|
||||||
|
fontsize = config.imgmath_font_size
|
||||||
|
baselineskip = int(round(fontsize * 1.2))
|
||||||
|
|
||||||
|
latex = DOC_HEAD + config.imgmath_latex_preamble
|
||||||
|
if config.imgmath_use_preview:
|
||||||
|
latex += DOC_BODY_PREVIEW % (fontsize, baselineskip, math)
|
||||||
|
else:
|
||||||
|
latex += DOC_BODY % (fontsize, baselineskip, math)
|
||||||
|
|
||||||
|
return latex
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_tempdir(builder):
|
||||||
|
# type: (Builder) -> unicode
|
||||||
|
"""Create temporary directory.
|
||||||
|
|
||||||
|
use only one tempdir per build -- the use of a directory is cleaner
|
||||||
|
than using temporary files, since we can clean up everything at once
|
||||||
|
just removing the whole directory (see cleanup_tempdir)
|
||||||
|
"""
|
||||||
|
if not hasattr(builder, '_imgmath_tempdir'):
|
||||||
|
builder._imgmath_tempdir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
return builder._imgmath_tempdir
|
||||||
|
|
||||||
|
|
||||||
|
def compile_math(latex, builder):
|
||||||
|
# type: (unicode, Builder) -> unicode
|
||||||
|
"""Compile LaTeX macros for math to DVI."""
|
||||||
|
tempdir = ensure_tempdir(builder)
|
||||||
|
filename = path.join(tempdir, 'math.tex')
|
||||||
|
with codecs.open(filename, 'w', 'utf-8') as f:
|
||||||
|
f.write(latex)
|
||||||
|
|
||||||
|
# build latex command; old versions of latex don't have the
|
||||||
|
# --output-directory option, so we have to manually chdir to the
|
||||||
|
# temp dir to run it.
|
||||||
|
command = [builder.config.imgmath_latex, '--interaction=nonstopmode']
|
||||||
|
# add custom args from the config file
|
||||||
|
command.extend(builder.config.imgmath_latex_args)
|
||||||
|
command.append('math.tex')
|
||||||
|
|
||||||
|
with cd(tempdir):
|
||||||
|
try:
|
||||||
|
p = Popen(command, stdout=PIPE, stderr=PIPE)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != ENOENT: # No such file or directory
|
||||||
|
raise
|
||||||
|
logger.warning('LaTeX command %r cannot be run (needed for math '
|
||||||
|
'display), check the imgmath_latex setting',
|
||||||
|
builder.config.imgmath_latex)
|
||||||
|
raise InvokeError
|
||||||
|
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise MathExtError('latex exited with error', stderr, stdout)
|
||||||
|
|
||||||
|
return path.join(tempdir, 'math.dvi')
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dvi_to_image(command, name):
|
||||||
|
# type: (List[unicode], unicode) -> Tuple[unicode, unicode]
|
||||||
|
"""Convert DVI file to specific image format."""
|
||||||
|
try:
|
||||||
|
p = Popen(command, stdout=PIPE, stderr=PIPE)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != ENOENT: # No such file or directory
|
||||||
|
raise
|
||||||
|
logger.warning('%s command %r cannot be run (needed for math '
|
||||||
|
'display), check the imgmath_%s setting',
|
||||||
|
name, command[0], name)
|
||||||
|
raise InvokeError
|
||||||
|
|
||||||
|
stdout, stderr = p.communicate()
|
||||||
|
if p.returncode != 0:
|
||||||
|
raise MathExtError('%s exited with error' % name, stderr, stdout)
|
||||||
|
|
||||||
|
return stdout, stderr
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dvi_to_png(dvipath, builder):
|
||||||
|
# type: (unicode, Builder) -> Tuple[unicode, int]
|
||||||
|
"""Convert DVI file to PNG image."""
|
||||||
|
tempdir = ensure_tempdir(builder)
|
||||||
|
filename = path.join(tempdir, 'math.png')
|
||||||
|
|
||||||
|
name = 'dvipng'
|
||||||
|
command = [builder.config.imgmath_dvipng, '-o', filename, '-T', 'tight', '-z9']
|
||||||
|
command.extend(builder.config.imgmath_dvipng_args)
|
||||||
|
if builder.config.imgmath_use_preview:
|
||||||
|
command.append('--depth')
|
||||||
|
command.append(dvipath)
|
||||||
|
|
||||||
|
stdout, stderr = convert_dvi_to_image(command, name)
|
||||||
|
|
||||||
|
depth = None
|
||||||
|
if builder.config.imgmath_use_preview:
|
||||||
|
for line in stdout.splitlines():
|
||||||
|
matched = depth_re.match(line)
|
||||||
|
if matched:
|
||||||
|
depth = int(matched.group(1))
|
||||||
|
write_png_depth(filename, depth)
|
||||||
|
break
|
||||||
|
|
||||||
|
return filename, depth
|
||||||
|
|
||||||
|
|
||||||
|
def convert_dvi_to_svg(dvipath, builder):
|
||||||
|
# type: (unicode, Builder) -> Tuple[unicode, int]
|
||||||
|
"""Convert DVI file to SVG image."""
|
||||||
|
tempdir = ensure_tempdir(builder)
|
||||||
|
filename = path.join(tempdir, 'math.svg')
|
||||||
|
|
||||||
|
name = 'dvisvgm'
|
||||||
|
command = [builder.config.imgmath_dvisvgm, '-o', filename]
|
||||||
|
command.extend(builder.config.imgmath_dvisvgm_args)
|
||||||
|
command.append(dvipath)
|
||||||
|
|
||||||
|
convert_dvi_to_image(command, name)
|
||||||
|
return filename, None
|
||||||
|
|
||||||
|
|
||||||
def render_math(self, math):
|
def render_math(self, math):
|
||||||
# type: (nodes.NodeVisitor, unicode) -> Tuple[unicode, int]
|
# type: (nodes.NodeVisitor, unicode) -> Tuple[unicode, int]
|
||||||
"""Render the LaTeX math expression *math* using latex and dvipng or
|
"""Render the LaTeX math expression *math* using latex and dvipng or
|
||||||
@ -97,20 +228,15 @@ def render_math(self, math):
|
|||||||
docs successfully). If the programs are there, however, they may not fail
|
docs successfully). If the programs are there, however, they may not fail
|
||||||
since that indicates a problem in the math source.
|
since that indicates a problem in the math source.
|
||||||
"""
|
"""
|
||||||
image_format = self.builder.config.imgmath_image_format
|
image_format = self.builder.config.imgmath_image_format.lower()
|
||||||
if image_format not in ('png', 'svg'):
|
if image_format not in SUPPORT_FORMAT:
|
||||||
raise MathExtError(
|
raise MathExtError('imgmath_image_format must be either "png" or "svg"')
|
||||||
'imgmath_image_format must be either "png" or "svg"')
|
|
||||||
|
|
||||||
font_size = self.builder.config.imgmath_font_size
|
latex = generate_latex_macro(math, self.builder.config)
|
||||||
use_preview = self.builder.config.imgmath_use_preview
|
|
||||||
latex = DOC_HEAD + self.builder.config.imgmath_latex_preamble
|
|
||||||
latex += (use_preview and DOC_BODY_PREVIEW or DOC_BODY) % (
|
|
||||||
font_size, int(round(font_size * 1.2)), math)
|
|
||||||
|
|
||||||
shasum = "%s.%s" % (sha1(latex.encode('utf-8')).hexdigest(), image_format)
|
filename = "%s.%s" % (sha1(latex.encode('utf-8')).hexdigest(), image_format)
|
||||||
relfn = posixpath.join(self.builder.imgpath, 'math', shasum)
|
relfn = posixpath.join(self.builder.imgpath, 'math', filename)
|
||||||
outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', shasum)
|
outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', filename)
|
||||||
if path.isfile(outfn):
|
if path.isfile(outfn):
|
||||||
depth = read_png_depth(outfn)
|
depth = read_png_depth(outfn)
|
||||||
return relfn, depth
|
return relfn, depth
|
||||||
@ -120,91 +246,26 @@ def render_math(self, math):
|
|||||||
hasattr(self.builder, '_imgmath_warned_image_translator'):
|
hasattr(self.builder, '_imgmath_warned_image_translator'):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
# use only one tempdir per build -- the use of a directory is cleaner
|
# .tex -> .dvi
|
||||||
# than using temporary files, since we can clean up everything at once
|
|
||||||
# just removing the whole directory (see cleanup_tempdir)
|
|
||||||
if not hasattr(self.builder, '_imgmath_tempdir'):
|
|
||||||
tempdir = self.builder._imgmath_tempdir = tempfile.mkdtemp()
|
|
||||||
else:
|
|
||||||
tempdir = self.builder._imgmath_tempdir
|
|
||||||
|
|
||||||
with codecs.open(path.join(tempdir, 'math.tex'), 'w', 'utf-8') as tf:
|
|
||||||
tf.write(latex)
|
|
||||||
|
|
||||||
# build latex command; old versions of latex don't have the
|
|
||||||
# --output-directory option, so we have to manually chdir to the
|
|
||||||
# temp dir to run it.
|
|
||||||
ltx_args = [self.builder.config.imgmath_latex, '--interaction=nonstopmode']
|
|
||||||
# add custom args from the config file
|
|
||||||
ltx_args.extend(self.builder.config.imgmath_latex_args)
|
|
||||||
ltx_args.append('math.tex')
|
|
||||||
|
|
||||||
with cd(tempdir):
|
|
||||||
try:
|
|
||||||
p = Popen(ltx_args, stdout=PIPE, stderr=PIPE)
|
|
||||||
except OSError as err:
|
|
||||||
if err.errno != ENOENT: # No such file or directory
|
|
||||||
raise
|
|
||||||
logger.warning('LaTeX command %r cannot be run (needed for math '
|
|
||||||
'display), check the imgmath_latex setting',
|
|
||||||
self.builder.config.imgmath_latex)
|
|
||||||
self.builder._imgmath_warned_latex = True
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
|
||||||
if p.returncode != 0:
|
|
||||||
raise MathExtError('latex exited with error', stderr, stdout)
|
|
||||||
|
|
||||||
ensuredir(path.dirname(outfn))
|
|
||||||
if image_format == 'png':
|
|
||||||
image_translator = 'dvipng'
|
|
||||||
image_translator_executable = self.builder.config.imgmath_dvipng
|
|
||||||
# use some standard dvipng arguments
|
|
||||||
image_translator_args = [self.builder.config.imgmath_dvipng]
|
|
||||||
image_translator_args += ['-o', outfn, '-T', 'tight', '-z9']
|
|
||||||
# add custom ones from config value
|
|
||||||
image_translator_args.extend(self.builder.config.imgmath_dvipng_args)
|
|
||||||
if use_preview:
|
|
||||||
image_translator_args.append('--depth')
|
|
||||||
elif image_format == 'svg':
|
|
||||||
image_translator = 'dvisvgm'
|
|
||||||
image_translator_executable = self.builder.config.imgmath_dvisvgm
|
|
||||||
# use some standard dvisvgm arguments
|
|
||||||
image_translator_args = [self.builder.config.imgmath_dvisvgm]
|
|
||||||
image_translator_args += ['-o', outfn]
|
|
||||||
# add custom ones from config value
|
|
||||||
image_translator_args.extend(self.builder.config.imgmath_dvisvgm_args)
|
|
||||||
else:
|
|
||||||
raise MathExtError(
|
|
||||||
'imgmath_image_format must be either "png" or "svg"')
|
|
||||||
|
|
||||||
# last, the input file name
|
|
||||||
image_translator_args.append(path.join(tempdir, 'math.dvi'))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p = Popen(image_translator_args, stdout=PIPE, stderr=PIPE)
|
dvipath = compile_math(latex, self.builder)
|
||||||
except OSError as err:
|
except InvokeError:
|
||||||
if err.errno != ENOENT: # No such file or directory
|
self.builder._imgmath_warned_latex = True
|
||||||
raise
|
return None, None
|
||||||
logger.warning('%s command %r cannot be run (needed for math '
|
|
||||||
'display), check the imgmath_%s setting',
|
# .dvi -> .png/.svg
|
||||||
image_translator, image_translator_executable,
|
try:
|
||||||
image_translator)
|
if image_format == 'png':
|
||||||
|
imgpath, depth = convert_dvi_to_png(dvipath, self.builder)
|
||||||
|
elif image_format == 'svg':
|
||||||
|
imgpath, depth = convert_dvi_to_svg(dvipath, self.builder)
|
||||||
|
except InvokeError:
|
||||||
self.builder._imgmath_warned_image_translator = True
|
self.builder._imgmath_warned_image_translator = True
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
stdout, stderr = p.communicate()
|
# Move generated image on tempdir to build dir
|
||||||
if p.returncode != 0:
|
ensuredir(path.dirname(outfn))
|
||||||
raise MathExtError('%s exited with error' %
|
shutil.move(imgpath, outfn)
|
||||||
image_translator, stderr, stdout)
|
|
||||||
depth = None
|
|
||||||
if use_preview and image_format == 'png': # depth is only useful for png
|
|
||||||
for line in stdout.splitlines():
|
|
||||||
m = depth_re.match(line)
|
|
||||||
if m:
|
|
||||||
depth = int(m.group(1))
|
|
||||||
write_png_depth(outfn, depth)
|
|
||||||
break
|
|
||||||
|
|
||||||
return relfn, depth
|
return relfn, depth
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user