diff --git a/CHANGES b/CHANGES index 19990fb0b..5942239a3 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,8 @@ Bugs fixed * Emit wrong warnings if citation label includes hyphens (refs: #3565) * #3858: Some warnings are not colored when using --color option * #3775: Remove unwanted whitespace in default template +* #3835: sphinx.ext.imgmath fails to convert SVG images if project directory + name contains spaces Testing -------- diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index 4e6bdd035..843b03f40 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -52,6 +52,12 @@ class MathExtError(SphinxError): SphinxError.__init__(self, msg) +class InvokeError(SphinxError): + """errors on invoking converters.""" + + +SUPPORT_FORMAT = ('png', 'svg') + DOC_HEAD = r''' \documentclass[12pt]{article} \usepackage[utf8x]{inputenc} @@ -82,6 +88,131 @@ DOC_BODY_PREVIEW = r''' 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): # type: (nodes.NodeVisitor, unicode) -> Tuple[unicode, int] """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 since that indicates a problem in the math source. """ - image_format = self.builder.config.imgmath_image_format - if image_format not in ('png', 'svg'): - raise MathExtError( - 'imgmath_image_format must be either "png" or "svg"') + image_format = self.builder.config.imgmath_image_format.lower() + if image_format not in SUPPORT_FORMAT: + raise MathExtError('imgmath_image_format must be either "png" or "svg"') - font_size = self.builder.config.imgmath_font_size - 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) + latex = generate_latex_macro(math, self.builder.config) - shasum = "%s.%s" % (sha1(latex.encode('utf-8')).hexdigest(), image_format) - relfn = posixpath.join(self.builder.imgpath, 'math', shasum) - outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', shasum) + filename = "%s.%s" % (sha1(latex.encode('utf-8')).hexdigest(), image_format) + relfn = posixpath.join(self.builder.imgpath, 'math', filename) + outfn = path.join(self.builder.outdir, self.builder.imagedir, 'math', filename) if path.isfile(outfn): depth = read_png_depth(outfn) return relfn, depth @@ -120,91 +246,26 @@ def render_math(self, math): hasattr(self.builder, '_imgmath_warned_image_translator'): return None, None - # 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(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')) - + # .tex -> .dvi try: - p = Popen(image_translator_args, 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', - image_translator, image_translator_executable, - image_translator) + dvipath = compile_math(latex, self.builder) + except InvokeError: + self.builder._imgmath_warned_latex = True + return None, None + + # .dvi -> .png/.svg + try: + 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 return None, None - stdout, stderr = p.communicate() - if p.returncode != 0: - raise MathExtError('%s exited with error' % - 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 + # Move generated image on tempdir to build dir + ensuredir(path.dirname(outfn)) + shutil.move(imgpath, outfn) return relfn, depth