From c5257238075fa4fbcb774fd2905a502d88070d8c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 7 Mar 2017 14:10:23 +0900 Subject: [PATCH] Add sphinx.ext.imgconverter --- .travis.yml | 1 + CHANGES | 2 + doc/ext/builtins.rst | 1 + doc/ext/imgconverter.rst | 25 +++ sphinx/ext/imgconverter.py | 88 +++++++++++ sphinx/transforms/post_transforms/images.py | 99 ++++++++++++ tests/roots/test-ext-imgconverter/conf.py | 4 + tests/roots/test-ext-imgconverter/index.rst | 4 + tests/roots/test-ext-imgconverter/svgimg.svg | 158 +++++++++++++++++++ tests/test_ext_imgconverter.py | 22 +++ 10 files changed, 404 insertions(+) create mode 100644 doc/ext/imgconverter.rst create mode 100644 sphinx/ext/imgconverter.py create mode 100644 tests/roots/test-ext-imgconverter/conf.py create mode 100644 tests/roots/test-ext-imgconverter/index.rst create mode 100644 tests/roots/test-ext-imgconverter/svgimg.svg create mode 100644 tests/test_ext_imgconverter.py diff --git a/.travis.yml b/.travis.yml index 3557095dd..c1b320dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ addons: - texlive-xetex - lmodern - latex-xcolor + - imagemagick install: - pip install -U pip setuptools - pip install docutils==$DOCUTILS diff --git a/CHANGES b/CHANGES index 99e5e2285..11b3ca5cd 100644 --- a/CHANGES +++ b/CHANGES @@ -117,6 +117,8 @@ Features added * #3641: Epub theme supports HTML structures that are generated by HTML5 writer. * #3644 autodoc uses inspect instead of checking types. Thanks to Jeroen Demeyer. +* Add a new extension; ``sphinx.ext.imgconverter``. It converts images in the + document to appropriate format for builders Bugs fixed ---------- diff --git a/doc/ext/builtins.rst b/doc/ext/builtins.rst index 6d5e59a89..6972a5957 100644 --- a/doc/ext/builtins.rst +++ b/doc/ext/builtins.rst @@ -15,6 +15,7 @@ These extensions are built in and can be activated by respective entries in the githubpages graphviz ifconfig + imgconverter inheritance intersphinx linkcode diff --git a/doc/ext/imgconverter.rst b/doc/ext/imgconverter.rst new file mode 100644 index 000000000..1dfb79cf7 --- /dev/null +++ b/doc/ext/imgconverter.rst @@ -0,0 +1,25 @@ +.. highlight:: rest + +:mod:`sphinx.ext.imgconverter` -- Convert images to appropriate format for builders +=================================================================================== + +.. module:: sphinx.ext.imgconverter + :synopsis: Convert images to appropriate format for builders + +.. versionadded:: 1.6 + +This extension converts images in your document to appropriate format for builders. +For example, it allows you to use SVG images with LaTeX builder. +As a result, you don't mind what image format the builder supports. + +Internally, this extension uses Imagemagick_ to convert images. + +.. _Imagemagick: https://www.imagemagick.org/script/index.php + +Configuration +------------- + +.. confval:: image_converter + + A path to :command:`convert` command. By default, the imgconverter uses + the command from search paths. diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py new file mode 100644 index 000000000..bc2564ee0 --- /dev/null +++ b/sphinx/ext/imgconverter.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.imgconverter + ~~~~~~~~~~~~~~~~~~~~~~~ + + Image converter extension for Sphinx + + :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import subprocess + +from sphinx.errors import ExtensionError +from sphinx.locale import _ +from sphinx.transforms.post_transforms.images import ImageConverter +from sphinx.util import logging +from sphinx.util.osutil import ENOENT, EPIPE, EINVAL + +if False: + # For type annotation + from typing import Any, Dict # NOQA + from sphinx.application import Sphinx # NOQA + + +logger = logging.getLogger(__name__) + + +class ImagemagickConverter(ImageConverter): + conversion_rules = [ + ('image/svg+xml', 'image/png'), + ('application/pdf', 'image/png'), + ] + + def is_available(self): + # type: () -> bool + """Confirms the converter is available or not.""" + try: + ret = subprocess.call([self.config.image_converter, '-version'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + if ret == 0: + return True + else: + return False + except (OSError, IOError): + logger.warning(_('convert command %r cannot be run.' + 'check the image_converter setting'), + self.config.image_converter) + return False + + def convert(self, _from, _to): + # type: (unicode, unicode) -> None + """Converts the image to expected one.""" + try: + p = subprocess.Popen([self.config.image_converter, _from, _to], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + except OSError as err: + if err.errno != ENOENT: # No such file or directory + raise + logger.warning(_('convert command %r cannot be run.' + 'check the image_converter setting'), + self.config.image_converter) + return False + + try: + stdout, stderr = p.communicate() + except (OSError, IOError) as err: + if err.errno not in (EPIPE, EINVAL): + raise + stdout, stderr = p.stdout.read(), p.stderr.read() + p.wait() + if p.returncode != 0: + raise ExtensionError(_('convert exited with error:\n' + '[stderr]\n%s\n[stdout]\n%s') % + (stderr, stdout)) + + return True + + +def setup(app): + # type: (Sphinx) -> Dict[unicode, Any] + app.add_post_transform(ImagemagickConverter) + app.add_config_value('image_converter', 'convert', 'env') + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index 9a232c1d4..cf481f62e 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -136,6 +136,105 @@ class DataURIExtractor(BaseImageConverter): self.app.env.images.add_file(self.env.docname, path) +def get_filename_for(filename, mimetype): + # type: (unicode, unicode) -> unicode + basename = os.path.basename(filename) + return os.path.splitext(basename)[0] + get_image_extension(mimetype) + + +class ImageConverter(BaseImageConverter): + """A base class images converter. + + The concrete image converters should derive this class and + overrides the following methods and attributes: + + * default_priority (if needed) + * conversion_rules + * is_available() + * convert() + """ + default_priority = 200 + + #: A conversion rules between two mimetypes which this converters supports + conversion_rules = [] + + def __init__(self, *args, **kwargs): + # type: (Any, Any) -> None + self.available = None # not checked yet + BaseImageConverter.__init__(self, *args, **kwargs) + + def match(self, node): + # type: (nodes.Node) -> bool + if self.available is None: + self.available = self.is_available() + + if not self.available: + return False + elif set(node['candidates']) & set(self.app.builder.supported_image_types): + # builder supports the image; no need to convert + return False + else: + rule = self.get_conversion_rule(node) + if rule: + return True + else: + return False + + def get_conversion_rule(self, node): + # type: (nodes.Node) -> Tuple[unicode, unicode] + for candidate in self.guess_mimetypes(node): + for supported in self.app.builder.supported_image_types: + rule = (candidate, supported) + if rule in self.conversion_rules: + return rule + + return None + + def is_available(self): + # type: () -> bool + """Confirms the converter is available or not.""" + raise NotImplemented + + def guess_mimetypes(self, node): + # type: (nodes.Node) -> unicode + if '?' in node['candidates']: + return [] + elif '*' in node['candidates']: + from sphinx.util.images import guess_mimetype + return [guess_mimetype(node['uri'])] + else: + return node['candidates'].keys() + + def handle(self, node): + # type: (nodes.Node) -> None + _from, _to = self.get_conversion_rule(node) + + if _from in node['candidates']: + srcpath = node['candidates'][_from] + else: + srcpath = node['candidates']['*'] + + filename = get_filename_for(srcpath, _to) + ensuredir(self.imagedir) + destpath = os.path.join(self.imagedir, filename) + + abs_srcpath = os.path.join(self.app.srcdir, srcpath) + if self.convert(abs_srcpath, destpath): + if '*' in node['candidates']: + node['candidates']['*'] = destpath + else: + node['candidates'][_to] = destpath + node['uri'] = destpath + + self.env.original_image_uri[destpath] = srcpath + self.env.images.add_file(self.env.docname, destpath) + + def convert(self, _from, _to): + # type: (unicode, unicode) -> None + """Converts the image to expected one.""" + raise NotImplemented + + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_post_transform(ImageDownloader) diff --git a/tests/roots/test-ext-imgconverter/conf.py b/tests/roots/test-ext-imgconverter/conf.py new file mode 100644 index 000000000..67cee152d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/conf.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +master_doc = 'index' +extensions = ['sphinx.ext.imgconverter'] diff --git a/tests/roots/test-ext-imgconverter/index.rst b/tests/roots/test-ext-imgconverter/index.rst new file mode 100644 index 000000000..786c92e8d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/index.rst @@ -0,0 +1,4 @@ +test-ext-imgconverter +===================== + +.. image:: svgimg.svg diff --git a/tests/roots/test-ext-imgconverter/svgimg.svg b/tests/roots/test-ext-imgconverter/svgimg.svg new file mode 100644 index 000000000..10e035b6d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/svgimg.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004) + + + +
  • + + + + + + </Agent> + </publisher> + <creator + id="creator24"> + <Agent + about="" + id="Agent25"> + <title + id="title26">Danny Allen + + + + + Danny Allen + + + + image/svg+xml + + + + + en + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/tests/test_ext_imgconverter.py b/tests/test_ext_imgconverter.py new file mode 100644 index 000000000..3ea396093 --- /dev/null +++ b/tests/test_ext_imgconverter.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" + test_ext_imgconverter + ~~~~~~~~~~~~~~~~~~~~~ + + Test sphinx.ext.imgconverter extension. + + :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + + +@pytest.mark.sphinx('latex', testroot='ext-imgconverter') +def test_ext_imgconverter(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'Python.tex').text() + assert '\sphinxincludegraphics{{svgimg}.png}' in content + assert not (app.outdir / 'svgimg.svg').exists() + assert (app.outdir / 'svgimg.png').exists()