1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
|
# $Id: images.py 10102 2025-04-23 15:54:44Z milde $
# Author: David Goodger <goodger@python.org>
# Copyright: This module has been placed in the public domain.
"""
Directives for figures and simple images.
"""
__docformat__ = 'reStructuredText'
from urllib.request import url2pathname
try: # check for the Python Imaging Library
import PIL.Image
except ImportError:
try: # sometimes PIL modules are put in PYTHONPATH's root
import Image
class PIL: pass # noqa:E701 dummy wrapper
PIL.Image = Image
except ImportError:
PIL = None
from docutils import nodes
from docutils.nodes import fully_normalize_name, whitespace_normalize_name
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives, states
from docutils.parsers.rst.roles import normalize_options
class Image(Directive):
align_h_values = ('left', 'center', 'right')
align_v_values = ('top', 'middle', 'bottom')
align_values = align_v_values + align_h_values
loading_values = ('embed', 'link', 'lazy')
def align(argument):
# This is not callable as `self.align()`. We cannot make it a
# staticmethod because we're saving an unbound method in
# option_spec below.
return directives.choice(argument, Image.align_values)
def loading(argument):
# This is not callable as `self.loading()` (see above).
return directives.choice(argument, Image.loading_values)
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
option_spec = {'alt': directives.unchanged,
'height': directives.length_or_unitless,
'width': directives.length_or_percentage_or_unitless,
'scale': directives.percentage,
'align': align,
'target': directives.unchanged_required,
'loading': loading,
'class': directives.class_option,
'name': directives.unchanged}
def run(self):
if 'align' in self.options:
if isinstance(self.state, states.SubstitutionDef):
# Check for align_v_values.
if self.options['align'] not in self.align_v_values:
raise self.error(
'Error in "%s" directive: "%s" is not a valid value '
'for the "align" option within a substitution '
'definition. Valid values for "align" are: "%s".'
% (self.name, self.options['align'],
'", "'.join(self.align_v_values)))
elif self.options['align'] not in self.align_h_values:
raise self.error(
'Error in "%s" directive: "%s" is not a valid value for '
'the "align" option. Valid values for "align" are: "%s".'
% (self.name, self.options['align'],
'", "'.join(self.align_h_values)))
messages = []
reference = directives.uri(self.arguments[0])
self.options['uri'] = reference
reference_node = None
if 'target' in self.options:
block = states.escape2null(
self.options['target']).splitlines()
block = list(block)
target_type, data = self.state.parse_target(
block, self.block_text, self.lineno)
if target_type == 'refuri':
reference_node = nodes.reference(refuri=data)
elif target_type == 'refname':
reference_node = nodes.reference(
refname=fully_normalize_name(data),
name=whitespace_normalize_name(data))
reference_node.indirect_reference_name = data
self.state.document.note_refname(reference_node)
else: # malformed target
messages.append(data) # data is a system message
del self.options['target']
options = normalize_options(self.options)
image_node = nodes.image(self.block_text, **options)
(image_node.source,
image_node.line) = self.state_machine.get_source_and_line(self.lineno)
self.add_name(image_node)
if reference_node:
reference_node += image_node
return messages + [reference_node]
else:
return messages + [image_node]
class Figure(Image):
def align(argument):
return directives.choice(argument, Figure.align_h_values)
def figwidth_value(argument):
if argument.lower() == 'image':
return 'image'
else:
return directives.length_or_percentage_or_unitless(argument, 'px')
option_spec = Image.option_spec.copy()
option_spec['figwidth'] = figwidth_value
option_spec['figclass'] = directives.class_option
option_spec['figname'] = directives.unchanged
option_spec['align'] = align
has_content = True
def run(self):
figwidth = self.options.pop('figwidth', None)
figclasses = self.options.pop('figclass', None)
figname = self.options.pop('figname', None)
align = self.options.pop('align', None)
(image_node,) = Image.run(self)
if isinstance(image_node, nodes.system_message):
return [image_node]
figure_node = nodes.figure('', image_node)
(figure_node.source, figure_node.line
) = self.state_machine.get_source_and_line(self.lineno)
if figwidth == 'image':
if PIL and self.state.document.settings.file_insertion_enabled:
imagepath = url2pathname(image_node['uri'])
try:
with PIL.Image.open(imagepath) as img:
figure_node['width'] = '%dpx' % img.size[0]
except (OSError, UnicodeEncodeError):
pass # TODO: warn/info?
else:
self.state.document.settings.record_dependencies.add(
imagepath.replace('\\', '/'))
elif figwidth is not None:
figure_node['width'] = figwidth
if figclasses:
figure_node['classes'] += figclasses
if figname:
figure_node['names'].append(nodes.fully_normalize_name(figname))
self.state.document.note_explicit_target(figure_node, figure_node)
if align:
figure_node['align'] = align
if self.content:
# optional caption (single paragraph or empty comment)
# + optional legend (arbitrary body elements).
node = nodes.Element() # anonymous container for parsing
self.state.nested_parse(self.content, self.content_offset, node)
for i, child in enumerate(node):
# skip temporary nodes that will be removed by transforms
if isinstance(child, (nodes.target, nodes.pending)):
figure_node += child
continue
if isinstance(child, nodes.paragraph):
caption = nodes.caption(child.rawsource, '',
*child.children)
caption.source = child.source
caption.line = child.line
figure_node += caption
break
if isinstance(child, nodes.comment) and len(child) == 0:
break
error = self.reporter.error(
'Figure caption must be a paragraph or empty comment.',
nodes.literal_block(self.block_text, self.block_text),
line=self.lineno)
return [figure_node, error]
if len(node) > i+1:
figure_node += nodes.legend('', *node[i+1:])
return [figure_node]
|