mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge pull request #3589 from tk0miya/support_remote_images
Support remote images
This commit is contained in:
commit
45887c7d62
2
CHANGES
2
CHANGES
@ -107,6 +107,8 @@ Features added
|
||||
``suppress_warnings``
|
||||
* #2803: Discovery of builders by entry point
|
||||
* #1764, #1676: Allow setting 'rel' and 'title' attributes for stylesheets
|
||||
* #3589: Support remote images on non-HTML builders
|
||||
* #3589: Support images in Data URI on non-HTML builders
|
||||
* #2961: improve :confval:`autodoc_mock_imports`. Now the config value only
|
||||
requires to declare the top-level modules that should be mocked.
|
||||
Thanks to Robin Jarry.
|
||||
|
@ -227,8 +227,6 @@ General configuration
|
||||
* app.add_generic_role
|
||||
* app.add_source_parser
|
||||
* download.not_readable
|
||||
* image.data_uri
|
||||
* image.nonlocal_uri
|
||||
* image.not_readable
|
||||
* ref.term
|
||||
* ref.ref
|
||||
|
@ -93,6 +93,7 @@ builtin_extensions = (
|
||||
'sphinx.directives.patches',
|
||||
'sphinx.roles',
|
||||
'sphinx.transforms.post_transforms',
|
||||
'sphinx.transforms.post_transforms.images',
|
||||
# collectors should be loaded by specific order
|
||||
'sphinx.environment.collectors.dependencies',
|
||||
'sphinx.environment.collectors.asset',
|
||||
|
@ -61,6 +61,12 @@ class Builder(object):
|
||||
# support translation
|
||||
use_message_catalog = True
|
||||
|
||||
#: The list of MIME types of image formats supported by the builder.
|
||||
#: Image files are searched in the order in which they appear here.
|
||||
supported_image_types = [] # type: List[unicode]
|
||||
supported_remote_images = False
|
||||
supported_data_uri_images = False
|
||||
|
||||
def __init__(self, app):
|
||||
# type: (Sphinx) -> None
|
||||
self.srcdir = app.srcdir
|
||||
@ -157,10 +163,6 @@ class Builder(object):
|
||||
"""Return list of paths for assets (ex. templates, CSS, etc.)."""
|
||||
return []
|
||||
|
||||
#: The list of MIME types of image formats supported by the builder.
|
||||
#: Image files are searched in the order in which they appear here.
|
||||
supported_image_types = [] # type: List[unicode]
|
||||
|
||||
def post_process_images(self, doctree):
|
||||
# type: (nodes.Node) -> None
|
||||
"""Pick the best candidate for all image URIs."""
|
||||
|
@ -109,6 +109,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
copysource = False
|
||||
supported_image_types = ['image/svg+xml', 'image/png', 'image/gif',
|
||||
'image/jpeg']
|
||||
supported_remote_images = False
|
||||
|
||||
# don't add links
|
||||
add_permalinks = False
|
||||
|
@ -63,6 +63,7 @@ class Epub3Builder(_epub_base.EpubBuilder):
|
||||
"""
|
||||
name = 'epub'
|
||||
|
||||
supported_remote_images = False
|
||||
template_dir = path.join(package_dir, 'templates', 'epub3')
|
||||
doctype = DOCTYPE
|
||||
html_tag = HTML_TAG
|
||||
|
@ -47,6 +47,7 @@ from sphinx.highlighting import PygmentsBridge
|
||||
from sphinx.util.console import bold, darkgreen # type: ignore
|
||||
from sphinx.writers.html import HTMLWriter, HTMLTranslator, \
|
||||
SmartyPantsHTMLTranslator
|
||||
from sphinx.environment.adapters.asset import ImageAdapter
|
||||
from sphinx.environment.adapters.toctree import TocTree
|
||||
from sphinx.environment.adapters.indexentries import IndexEntries
|
||||
|
||||
@ -119,6 +120,8 @@ class StandaloneHTMLBuilder(Builder):
|
||||
html_scaled_image_link = True
|
||||
supported_image_types = ['image/svg+xml', 'image/png',
|
||||
'image/gif', 'image/jpeg']
|
||||
supported_remote_images = True
|
||||
supported_data_uri_images = True
|
||||
searchindex_filename = 'searchindex.js'
|
||||
add_permalinks = True
|
||||
allow_sharp_as_current_path = True
|
||||
@ -629,11 +632,12 @@ class StandaloneHTMLBuilder(Builder):
|
||||
|
||||
def copy_image_files(self):
|
||||
# type: () -> None
|
||||
# copy image files
|
||||
if self.images:
|
||||
stringify_func = ImageAdapter(self.app.env).get_original_image_uri
|
||||
ensuredir(path.join(self.outdir, self.imagedir))
|
||||
for src in status_iterator(self.images, 'copying images... ', "brown",
|
||||
len(self.images), self.app.verbosity):
|
||||
len(self.images), self.app.verbosity,
|
||||
stringify_func=stringify_func):
|
||||
dest = self.images[src]
|
||||
try:
|
||||
copyfile(path.join(self.srcdir, src),
|
||||
|
@ -13,8 +13,6 @@ import os
|
||||
import warnings
|
||||
from os import path
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.io import FileOutput
|
||||
from docutils.utils import new_document
|
||||
@ -22,12 +20,13 @@ from docutils.frontend import OptionParser
|
||||
|
||||
from sphinx import package_dir, addnodes, highlighting
|
||||
from sphinx.deprecation import RemovedInSphinx17Warning
|
||||
from sphinx.util import texescape, logging
|
||||
from sphinx.config import string_classes, ENUM
|
||||
from sphinx.errors import SphinxError
|
||||
from sphinx.locale import _
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.environment import NoUri
|
||||
from sphinx.environment.adapters.asset import ImageAdapter
|
||||
from sphinx.util import texescape, logging, status_iterator
|
||||
from sphinx.util.nodes import inline_all_toctrees
|
||||
from sphinx.util.fileutil import copy_asset_file
|
||||
from sphinx.util.osutil import SEP, make_filename
|
||||
@ -51,6 +50,7 @@ class LaTeXBuilder(Builder):
|
||||
name = 'latex'
|
||||
format = 'latex'
|
||||
supported_image_types = ['application/pdf', 'image/png', 'image/jpeg']
|
||||
supported_remote_images = False
|
||||
|
||||
def init(self):
|
||||
# type: () -> None
|
||||
@ -206,14 +206,7 @@ class LaTeXBuilder(Builder):
|
||||
|
||||
def finish(self):
|
||||
# type: () -> None
|
||||
# copy image files
|
||||
if self.images:
|
||||
logger.info(bold('copying images...'), nonl=1)
|
||||
for src, dest in iteritems(self.images):
|
||||
logger.info(' ' + src, nonl=1)
|
||||
copy_asset_file(path.join(self.srcdir, src),
|
||||
path.join(self.outdir, dest))
|
||||
logger.info('')
|
||||
self.copy_image_files()
|
||||
|
||||
# copy TeX support files from texinputs
|
||||
context = {'latex_engine': self.config.latex_engine}
|
||||
@ -240,6 +233,21 @@ class LaTeXBuilder(Builder):
|
||||
copy_asset_file(path.join(self.confdir, self.config.latex_logo), self.outdir)
|
||||
logger.info('done')
|
||||
|
||||
def copy_image_files(self):
|
||||
# type: () -> None
|
||||
if self.images:
|
||||
stringify_func = ImageAdapter(self.app.env).get_original_image_uri
|
||||
for src in status_iterator(self.images, 'copying images... ', "brown",
|
||||
len(self.images), self.app.verbosity,
|
||||
stringify_func=stringify_func):
|
||||
dest = self.images[src]
|
||||
try:
|
||||
copy_asset_file(path.join(self.srcdir, src),
|
||||
path.join(self.outdir, dest))
|
||||
except Exception as err:
|
||||
logger.warning('cannot copy image file %r: %s',
|
||||
path.join(self.srcdir, src), err)
|
||||
|
||||
|
||||
def validate_config_values(app):
|
||||
# type: (Sphinx) -> None
|
||||
|
@ -11,8 +11,6 @@
|
||||
|
||||
from os import path
|
||||
|
||||
from six import iteritems
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.io import FileOutput
|
||||
from docutils.utils import new_document
|
||||
@ -22,9 +20,12 @@ from sphinx import addnodes
|
||||
from sphinx.locale import _
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.environment import NoUri
|
||||
from sphinx.environment.adapters.asset import ImageAdapter
|
||||
from sphinx.util import logging
|
||||
from sphinx.util import status_iterator
|
||||
from sphinx.util.fileutil import copy_asset_file
|
||||
from sphinx.util.nodes import inline_all_toctrees
|
||||
from sphinx.util.osutil import SEP, copyfile, make_filename
|
||||
from sphinx.util.osutil import SEP, make_filename
|
||||
from sphinx.util.console import bold, darkgreen # type: ignore
|
||||
from sphinx.writers.texinfo import TexinfoWriter
|
||||
|
||||
@ -223,14 +224,7 @@ class TexinfoBuilder(Builder):
|
||||
|
||||
def finish(self):
|
||||
# type: () -> None
|
||||
# copy image files
|
||||
if self.images:
|
||||
logger.info(bold('copying images...'), nonl=1)
|
||||
for src, dest in iteritems(self.images):
|
||||
logger.info(' ' + src, nonl=1)
|
||||
copyfile(path.join(self.srcdir, src),
|
||||
path.join(self.outdir, dest))
|
||||
logger.info('')
|
||||
self.copy_image_files()
|
||||
|
||||
logger.info(bold('copying Texinfo support files... '), nonl=True)
|
||||
# copy Makefile
|
||||
@ -243,6 +237,21 @@ class TexinfoBuilder(Builder):
|
||||
logger.warning("error writing file %s: %s", fn, err)
|
||||
logger.info(' done')
|
||||
|
||||
def copy_image_files(self):
|
||||
# type: () -> None
|
||||
if self.images:
|
||||
stringify_func = ImageAdapter(self.app.env).get_original_image_uri
|
||||
for src in status_iterator(self.images, 'copying images... ', "brown",
|
||||
len(self.images), self.app.verbosity,
|
||||
stringify_func=stringify_func):
|
||||
dest = self.images[src]
|
||||
try:
|
||||
copy_asset_file(path.join(self.srcdir, src),
|
||||
path.join(self.outdir, dest))
|
||||
except Exception as err:
|
||||
logger.warning('cannot copy image file %r: %s',
|
||||
path.join(self.srcdir, src), err)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[unicode, Any]
|
||||
|
@ -257,6 +257,9 @@ class BuildEnvironment(object):
|
||||
self.images = FilenameUniqDict()
|
||||
self.dlfiles = FilenameUniqDict()
|
||||
|
||||
# the original URI for images
|
||||
self.original_image_uri = {} # type: Dict[unicode, unicode]
|
||||
|
||||
# temporary data storage while reading a document
|
||||
self.temp_data = {} # type: Dict[unicode, Any]
|
||||
# context for cross-references (e.g. current module or class)
|
||||
|
28
sphinx/environment/adapters/asset.py
Normal file
28
sphinx/environment/adapters/asset.py
Normal file
@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.environment.adapters.asset
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Assets adapter for sphinx.environment.
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from sphinx.environment import BuildEnvironment # NOQA
|
||||
|
||||
|
||||
class ImageAdapter(object):
|
||||
def __init__(self, env):
|
||||
# type: (BuildEnvironment) -> None
|
||||
self.env = env
|
||||
|
||||
def get_original_image_uri(self, name):
|
||||
# type: (unicode) -> unicode
|
||||
"""Get the original image URI."""
|
||||
while name in self.env.original_image_uri:
|
||||
name = self.env.original_image_uri[name]
|
||||
|
||||
return name
|
@ -59,14 +59,9 @@ class ImageCollector(EnvironmentCollector):
|
||||
node['candidates'] = candidates
|
||||
imguri = node['uri']
|
||||
if imguri.startswith('data:'):
|
||||
logger.warning('image data URI found. some builders might not support',
|
||||
location=node, type='image', subtype='data_uri')
|
||||
candidates['?'] = imguri
|
||||
continue
|
||||
elif imguri.find('://') != -1:
|
||||
logger.warning('nonlocal image URI found: %s' % imguri,
|
||||
location=node,
|
||||
type='image', subtype='nonlocal_uri')
|
||||
candidates['?'] = imguri
|
||||
continue
|
||||
rel_imgpath, full_imgpath = app.env.relfn2path(imguri, docname)
|
||||
|
148
sphinx/transforms/post_transforms/images.py
Normal file
148
sphinx/transforms/post_transforms/images.py
Normal file
@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.transforms.post_transforms.images
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Docutils transforms used by Sphinx.
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import os
|
||||
from math import ceil
|
||||
from hashlib import sha1
|
||||
|
||||
from six import text_type
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx.transforms import SphinxTransform
|
||||
from sphinx.util import logging, requests
|
||||
from sphinx.util import epoch_to_rfc1123, rfc1123_to_epoch
|
||||
from sphinx.util.images import guess_mimetype, get_image_extension, parse_data_uri
|
||||
from sphinx.util.osutil import ensuredir
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any, Dict # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseImageConverter(SphinxTransform):
|
||||
def apply(self):
|
||||
# type: () -> None
|
||||
for node in self.document.traverse(nodes.image):
|
||||
if self.match(node):
|
||||
self.handle(node)
|
||||
|
||||
def match(self, node):
|
||||
# type: (nodes.Node) -> bool
|
||||
return True
|
||||
|
||||
def handle(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
pass
|
||||
|
||||
@property
|
||||
def imagedir(self):
|
||||
# type: () -> unicode
|
||||
return os.path.join(self.app.doctreedir, 'images')
|
||||
|
||||
|
||||
class ImageDownloader(BaseImageConverter):
|
||||
default_priority = 100
|
||||
|
||||
def match(self, node):
|
||||
# type: (nodes.Node) -> bool
|
||||
if self.app.builder.supported_remote_images:
|
||||
return False
|
||||
else:
|
||||
return '://' in node['uri']
|
||||
|
||||
def handle(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
basename = os.path.basename(node['uri'])
|
||||
if '?' in basename:
|
||||
basename = basename.split('?')[0]
|
||||
dirname = node['uri'].replace('://', '/').translate({ord("?"): u"/",
|
||||
ord("&"): u"/"})
|
||||
ensuredir(os.path.join(self.imagedir, dirname))
|
||||
path = os.path.join(self.imagedir, dirname, basename)
|
||||
try:
|
||||
headers = {}
|
||||
if os.path.exists(path):
|
||||
timestamp = ceil(os.stat(path).st_mtime)
|
||||
headers['If-Modified-Since'] = epoch_to_rfc1123(timestamp)
|
||||
|
||||
r = requests.get(node['uri'], headers=headers)
|
||||
if r.status_code >= 400:
|
||||
logger.warning('Could not fetch remote image: %s [%d]' %
|
||||
(node['uri'], r.status_code))
|
||||
else:
|
||||
self.app.env.original_image_uri[path] = node['uri']
|
||||
|
||||
if r.status_code == 200:
|
||||
with open(path, 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
last_modified = r.headers.get('last-modified')
|
||||
if last_modified:
|
||||
timestamp = rfc1123_to_epoch(last_modified)
|
||||
os.utime(path, (timestamp, timestamp))
|
||||
|
||||
mimetype = guess_mimetype(path, default='*')
|
||||
node['candidates'].pop('?')
|
||||
node['candidates'][mimetype] = path
|
||||
node['uri'] = path
|
||||
self.app.env.images.add_file(self.env.docname, path)
|
||||
except Exception as exc:
|
||||
logger.warning('Could not fetch remote image: %s [%s]' %
|
||||
(node['uri'], text_type(exc)))
|
||||
|
||||
|
||||
class DataURIExtractor(BaseImageConverter):
|
||||
default_priority = 150
|
||||
|
||||
def match(self, node):
|
||||
# type: (nodes.Node) -> bool
|
||||
if self.app.builder.supported_data_uri_images:
|
||||
return False
|
||||
else:
|
||||
return 'data:' in node['uri']
|
||||
|
||||
def handle(self, node):
|
||||
# type: (nodes.Node) -> None
|
||||
image = parse_data_uri(node['uri'])
|
||||
ext = get_image_extension(image.mimetype)
|
||||
if ext is None:
|
||||
logger.warning('Unknown image format: %s...', node['uri'][:32],
|
||||
location=node)
|
||||
return
|
||||
|
||||
ensuredir(os.path.join(self.imagedir, 'embeded'))
|
||||
digest = sha1(image.data).hexdigest()
|
||||
path = os.path.join(self.imagedir, 'embeded', digest + ext)
|
||||
self.app.env.original_image_uri[path] = node['uri']
|
||||
|
||||
with open(path, 'wb') as f:
|
||||
f.write(image.data)
|
||||
|
||||
node['candidates'].pop('?')
|
||||
node['candidates'][image.mimetype] = path
|
||||
node['uri'] = path
|
||||
self.app.env.images.add_file(self.env.docname, path)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[unicode, Any]
|
||||
app.add_post_transform(ImageDownloader)
|
||||
app.add_post_transform(DataURIExtractor)
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
@ -19,12 +19,15 @@ import posixpath
|
||||
import traceback
|
||||
import unicodedata
|
||||
from os import path
|
||||
from time import mktime, strptime
|
||||
from codecs import BOM_UTF8
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
|
||||
from six import text_type, binary_type, itervalues
|
||||
from six.moves import range
|
||||
from six.moves.urllib.parse import urlsplit, urlunsplit, quote_plus, parse_qsl, urlencode
|
||||
from babel.dates import format_datetime
|
||||
from docutils.utils import relative_path
|
||||
|
||||
from sphinx.errors import PycodeError, SphinxParallelError, ExtensionError
|
||||
@ -615,3 +618,14 @@ def status_iterator(iterable, summary, color="darkgreen", length=0, verbosity=0,
|
||||
yield item
|
||||
if l > 0:
|
||||
logger.info('')
|
||||
|
||||
|
||||
def epoch_to_rfc1123(epoch):
|
||||
"""Convert datetime format epoch to RFC1123."""
|
||||
dt = datetime.fromtimestamp(epoch)
|
||||
fmt = 'EEE, dd LLL yyyy hh:mm:ss'
|
||||
return format_datetime(dt, fmt, locale='en') + ' GMT'
|
||||
|
||||
|
||||
def rfc1123_to_epoch(rfc1123):
|
||||
return mktime(strptime(rfc1123, '%a, %d %b %Y %H:%M:%S %Z'))
|
||||
|
@ -8,10 +8,16 @@
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import imghdr
|
||||
import imagesize
|
||||
from os import path
|
||||
from collections import OrderedDict
|
||||
|
||||
from six import PY3, BytesIO, iteritems
|
||||
from typing import NamedTuple
|
||||
|
||||
try:
|
||||
from PIL import Image # check for the Python Imaging Library
|
||||
@ -23,13 +29,23 @@ except ImportError:
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Dict, List, Tuple # NOQA
|
||||
from typing import Dict, IO, List, Tuple # NOQA
|
||||
|
||||
mime_suffixes = {
|
||||
'.pdf': 'application/pdf',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.svgz': 'image/svg+xml',
|
||||
} # type: Dict[unicode, unicode]
|
||||
if PY3:
|
||||
unicode = str # special alias for static typing...
|
||||
|
||||
mime_suffixes = OrderedDict([
|
||||
('.gif', 'image/gif'),
|
||||
('.jpg', 'image/jpeg'),
|
||||
('.png', 'image/png'),
|
||||
('.pdf', 'application/pdf'),
|
||||
('.svg', 'image/svg+xml'),
|
||||
('.svgz', 'image/svg+xml'),
|
||||
]) # type: Dict[unicode, unicode]
|
||||
|
||||
DataURI = NamedTuple('DataURI', [('mimetype', unicode),
|
||||
('charset', unicode),
|
||||
('data', bytes)])
|
||||
|
||||
|
||||
def get_image_size(filename):
|
||||
@ -52,15 +68,55 @@ def get_image_size(filename):
|
||||
return None
|
||||
|
||||
|
||||
def guess_mimetype(filename):
|
||||
# type: (unicode) -> unicode
|
||||
_, ext = path.splitext(filename)
|
||||
def guess_mimetype_for_stream(stream, default=None):
|
||||
# type: (IO, unicode) -> unicode
|
||||
imgtype = imghdr.what(stream)
|
||||
if imgtype:
|
||||
return 'image/' + imgtype
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
def guess_mimetype(filename='', content=None, default=None):
|
||||
# type: (unicode, unicode, unicode) -> unicode
|
||||
_, ext = path.splitext(filename.lower())
|
||||
if ext in mime_suffixes:
|
||||
return mime_suffixes[ext]
|
||||
else:
|
||||
elif content:
|
||||
return guess_mimetype_for_stream(BytesIO(content), default=default)
|
||||
elif path.exists(filename):
|
||||
with open(filename, 'rb') as f:
|
||||
imgtype = imghdr.what(f)
|
||||
if imgtype:
|
||||
return 'image/' + imgtype
|
||||
return guess_mimetype_for_stream(f, default=default)
|
||||
|
||||
return default
|
||||
|
||||
|
||||
def get_image_extension(mimetype):
|
||||
# type: (unicode) -> unicode
|
||||
for ext, _mimetype in iteritems(mime_suffixes):
|
||||
if mimetype == _mimetype:
|
||||
return ext
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def parse_data_uri(uri):
|
||||
# type: (unicode) -> DataURI
|
||||
if not uri.startswith('data:'):
|
||||
return None
|
||||
|
||||
# data:[<MIME-type>][;charset=<encoding>][;base64],<data>
|
||||
mimetype = u'text/plain'
|
||||
charset = u'US-ASCII'
|
||||
|
||||
properties, data = uri[5:].split(',', 1)
|
||||
for prop in properties.split(';'):
|
||||
if prop == 'base64':
|
||||
pass # skip
|
||||
elif prop.startswith('charset='):
|
||||
charset = prop[8:]
|
||||
elif prop:
|
||||
mimetype = prop
|
||||
|
||||
image_data = base64.b64decode(data) # type: ignore
|
||||
return DataURI(mimetype, charset, image_data)
|
||||
|
@ -16,7 +16,7 @@ Sphinx image handling
|
||||
.. image:: img.*
|
||||
|
||||
.. a non-local image URI
|
||||
.. image:: http://www.python.org/logo.png
|
||||
.. image:: https://www.python.org/static/img/python-logo.png
|
||||
|
||||
.. an image with subdir and unspecified extension
|
||||
.. image:: subdir/simg.*
|
||||
|
@ -14,3 +14,9 @@ test-image
|
||||
The caption of img
|
||||
|
||||
.. image:: testimäge.png
|
||||
|
||||
.. a remote image
|
||||
.. image:: https://www.python.org/static/img/python-logo.png
|
||||
|
||||
.. non-exist remote image
|
||||
.. image:: http://example.com/NOT_EXIST.PNG
|
||||
|
@ -15,9 +15,6 @@ test-warnings
|
||||
.. an SVG image (for HTML at least)
|
||||
.. image:: svgimg.*
|
||||
|
||||
.. a non-local image URI
|
||||
.. image:: http://www.python.org/logo.png
|
||||
|
||||
.. should give a warning
|
||||
.. literalinclude:: wrongenc.inc
|
||||
:language: none
|
||||
|
@ -32,7 +32,6 @@ WARNING: Explicit markup ends without a blank line; unexpected unindent.
|
||||
%(root)s/index.rst:\\d+: WARNING: Encoding 'utf-8-sig' used for reading included \
|
||||
file u'%(root)s/wrongenc.inc' seems to be wrong, try giving an :encoding: option
|
||||
%(root)s/index.rst:\\d+: WARNING: image file not readable: foo.png
|
||||
%(root)s/index.rst:\\d+: WARNING: nonlocal image URI found: http://www.python.org/logo.png
|
||||
%(root)s/index.rst:\\d+: WARNING: download file not readable: %(root)s/nonexisting.png
|
||||
%(root)s/index.rst:\\d+: WARNING: invalid single index entry u''
|
||||
%(root)s/undecodable.rst:\\d+: WARNING: undecodable source characters, replacing \
|
||||
@ -1224,3 +1223,13 @@ def test_html_raw_directive(app, status, warning):
|
||||
def test_alternate_stylesheets(app, cached_etree_parse, fname, expect):
|
||||
app.build()
|
||||
check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='images')
|
||||
def test_html_remote_images(app, status, warning):
|
||||
app.builder.build_all()
|
||||
|
||||
result = (app.outdir / 'index.html').text(encoding='utf8')
|
||||
assert ('<img alt="https://www.python.org/static/img/python-logo.png" '
|
||||
'src="https://www.python.org/static/img/python-logo.png" />' in result)
|
||||
assert not (app.outdir / 'python-logo.png').exists()
|
||||
|
@ -1042,3 +1042,15 @@ def test_latex_raw_directive(app, status, warning):
|
||||
# with substitution
|
||||
assert 'HTML: abc ghi' in result
|
||||
assert 'LaTeX: abc def ghi' in result
|
||||
|
||||
|
||||
@pytest.mark.sphinx('latex', testroot='images')
|
||||
def test_latex_remote_images(app, status, warning):
|
||||
app.builder.build_all()
|
||||
|
||||
result = (app.outdir / 'Python.tex').text(encoding='utf8')
|
||||
assert '\\sphinxincludegraphics{{python-logo}.png}' in result
|
||||
assert (app.outdir / 'python-logo.png').exists()
|
||||
assert '\\sphinxincludegraphics{{NOT_EXIST}.PNG}' not in result
|
||||
assert ('WARNING: Could not fetch remote image: '
|
||||
'http://example.com/NOT_EXIST.PNG [404]' in warning.getvalue())
|
||||
|
@ -40,8 +40,6 @@ def test_first_update():
|
||||
def test_images():
|
||||
assert ('image file not readable: foo.png'
|
||||
in app._warning.getvalue())
|
||||
assert ('nonlocal image URI found: http://www.python.org/logo.png'
|
||||
in app._warning.getvalue())
|
||||
|
||||
tree = env.get_doctree('images')
|
||||
htmlbuilder = StandaloneHTMLBuilder(app)
|
||||
|
97
tests/test_util_images.py
Normal file
97
tests/test_util_images.py
Normal file
@ -0,0 +1,97 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_util_images
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Test images util.
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import pytest
|
||||
|
||||
from sphinx.util.images import (
|
||||
get_image_size, guess_mimetype, get_image_extension, parse_data_uri
|
||||
)
|
||||
|
||||
from util import rootdir
|
||||
|
||||
|
||||
GIF_FILENAME = rootdir / 'root' / 'img.gif'
|
||||
PNG_FILENAME = rootdir / 'root' / 'img.png'
|
||||
PDF_FILENAME = rootdir / 'root' / 'img.pdf'
|
||||
TXT_FILENAME = rootdir / 'root' / 'contents.txt'
|
||||
|
||||
|
||||
def test_get_image_size():
|
||||
assert get_image_size(GIF_FILENAME) == (200, 181)
|
||||
assert get_image_size(PNG_FILENAME) == (200, 181)
|
||||
assert get_image_size(PDF_FILENAME) is None
|
||||
assert get_image_size(TXT_FILENAME) is None
|
||||
|
||||
|
||||
def test_guess_mimetype():
|
||||
# guess by filename
|
||||
assert guess_mimetype('img.png') == 'image/png'
|
||||
assert guess_mimetype('img.jpg') == 'image/jpeg'
|
||||
assert guess_mimetype('img.txt') is None
|
||||
assert guess_mimetype('img.txt', default='text/plain') == 'text/plain'
|
||||
assert guess_mimetype('no_extension') is None
|
||||
assert guess_mimetype('IMG.PNG') == 'image/png'
|
||||
|
||||
# guess by content
|
||||
assert guess_mimetype(content=GIF_FILENAME.bytes()) == 'image/gif'
|
||||
assert guess_mimetype(content=PNG_FILENAME.bytes()) == 'image/png'
|
||||
assert guess_mimetype(content=PDF_FILENAME.bytes()) is None
|
||||
assert guess_mimetype(content=TXT_FILENAME.bytes()) is None
|
||||
assert guess_mimetype(content=TXT_FILENAME.bytes(), default='text/plain') == 'text/plain'
|
||||
|
||||
# the priority of params: filename > content > default
|
||||
assert guess_mimetype('img.png',
|
||||
content=GIF_FILENAME.bytes(),
|
||||
default='text/plain') == 'image/png'
|
||||
assert guess_mimetype('no_extension',
|
||||
content=GIF_FILENAME.bytes(),
|
||||
default='text/plain') == 'image/gif'
|
||||
assert guess_mimetype('no_extension',
|
||||
content=TXT_FILENAME.bytes(),
|
||||
default='text/plain') == 'text/plain'
|
||||
|
||||
|
||||
def test_get_image_extension():
|
||||
assert get_image_extension('image/png') == '.png'
|
||||
assert get_image_extension('image/jpeg') == '.jpg'
|
||||
assert get_image_extension('image/svg+xml') == '.svg'
|
||||
assert get_image_extension('text/plain') is None
|
||||
|
||||
|
||||
def test_parse_data_uri():
|
||||
# standard case
|
||||
uri = ("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
|
||||
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")
|
||||
image = parse_data_uri(uri)
|
||||
assert image is not None
|
||||
assert image.mimetype == 'image/png'
|
||||
assert image.charset == 'US-ASCII'
|
||||
|
||||
# no mimetype
|
||||
uri = ("data:charset=utf-8,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElE"
|
||||
"QVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")
|
||||
image = parse_data_uri(uri)
|
||||
assert image is not None
|
||||
assert image.mimetype == 'text/plain'
|
||||
assert image.charset == 'utf-8'
|
||||
|
||||
# non data URI
|
||||
uri = ("image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
|
||||
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")
|
||||
image = parse_data_uri(uri)
|
||||
assert image is None
|
||||
|
||||
# invalid data URI (no properties)
|
||||
with pytest.raises(ValueError):
|
||||
uri = ("data:iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4"
|
||||
"//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==")
|
||||
parse_data_uri(uri)
|
Loading…
Reference in New Issue
Block a user