Clean up record_dependencies feature.
[docutils.git] / docutils / parsers / rst / directives / images.py
blob7adda2edfcfc83eed1f8d32c7cb3dbd13eb07529
1 # $Id$
2 # Author: David Goodger <goodger@python.org>
3 # Copyright: This module has been placed in the public domain.
5 """
6 Directives for figures and simple images.
7 """
9 __docformat__ = 'reStructuredText'
12 import sys
13 import urllib
14 from docutils import nodes, utils
15 from docutils.parsers.rst import Directive
16 from docutils.parsers.rst import directives, states
17 from docutils.nodes import fully_normalize_name, whitespace_normalize_name
18 from docutils.parsers.rst.roles import set_classes
19 try: # check for the Python Imaging Library
20 import PIL
21 except ImportError:
22 try: # sometimes PIL modules are put in PYTHONPATH's root
23 import Image
24 class PIL(object): pass # dummy wrapper
25 PIL.Image = Image
26 except ImportError:
27 PIL = None
29 class Image(Directive):
31 align_h_values = ('left', 'center', 'right')
32 align_v_values = ('top', 'middle', 'bottom')
33 align_values = align_v_values + align_h_values
35 def align(argument):
36 # This is not callable as self.align. We cannot make it a
37 # staticmethod because we're saving an unbound method in
38 # option_spec below.
39 return directives.choice(argument, Image.align_values)
41 required_arguments = 1
42 optional_arguments = 0
43 final_argument_whitespace = True
44 option_spec = {'alt': directives.unchanged,
45 'height': directives.length_or_unitless,
46 'width': directives.length_or_percentage_or_unitless,
47 'scale': directives.percentage,
48 'align': align,
49 'name': directives.unchanged,
50 'target': directives.unchanged_required,
51 'class': directives.class_option}
53 def run(self):
54 if 'align' in self.options:
55 if isinstance(self.state, states.SubstitutionDef):
56 # Check for align_v_values.
57 if self.options['align'] not in self.align_v_values:
58 raise self.error(
59 'Error in "%s" directive: "%s" is not a valid value '
60 'for the "align" option within a substitution '
61 'definition. Valid values for "align" are: "%s".'
62 % (self.name, self.options['align'],
63 '", "'.join(self.align_v_values)))
64 elif self.options['align'] not in self.align_h_values:
65 raise self.error(
66 'Error in "%s" directive: "%s" is not a valid value for '
67 'the "align" option. Valid values for "align" are: "%s".'
68 % (self.name, self.options['align'],
69 '", "'.join(self.align_h_values)))
70 messages = []
71 reference = directives.uri(self.arguments[0])
72 self.options['uri'] = reference
73 reference_node = None
74 if 'target' in self.options:
75 block = states.escape2null(
76 self.options['target']).splitlines()
77 block = [line for line in block]
78 target_type, data = self.state.parse_target(
79 block, self.block_text, self.lineno)
80 if target_type == 'refuri':
81 reference_node = nodes.reference(refuri=data)
82 elif target_type == 'refname':
83 reference_node = nodes.reference(
84 refname=fully_normalize_name(data),
85 name=whitespace_normalize_name(data))
86 reference_node.indirect_reference_name = data
87 self.state.document.note_refname(reference_node)
88 else: # malformed target
89 messages.append(data) # data is a system message
90 del self.options['target']
91 set_classes(self.options)
92 image_node = nodes.image(self.block_text, **self.options)
93 self.add_name(image_node)
94 if reference_node:
95 reference_node += image_node
96 return messages + [reference_node]
97 else:
98 return messages + [image_node]
101 class Figure(Image):
103 def align(argument):
104 return directives.choice(argument, Figure.align_h_values)
106 def figwidth_value(argument):
107 if argument.lower() == 'image':
108 return 'image'
109 else:
110 return directives.length_or_percentage_or_unitless(argument, 'px')
112 option_spec = Image.option_spec.copy()
113 option_spec['figwidth'] = figwidth_value
114 option_spec['figclass'] = directives.class_option
115 option_spec['align'] = align
116 has_content = True
118 def run(self):
119 figwidth = self.options.pop('figwidth', None)
120 figclasses = self.options.pop('figclass', None)
121 align = self.options.pop('align', None)
122 (image_node,) = Image.run(self)
123 if isinstance(image_node, nodes.system_message):
124 return [image_node]
125 figure_node = nodes.figure('', image_node)
126 if figwidth == 'image':
127 if PIL and self.state.document.settings.file_insertion_enabled:
128 imagepath = urllib.url2pathname(image_node['uri'])
129 try:
130 img = PIL.Image.open(
131 imagepath.encode(sys.getfilesystemencoding()))
132 except (IOError, UnicodeEncodeError):
133 pass # TODO: warn?
134 else:
135 self.state.document.settings.record_dependencies.add(
136 imagepath.replace('\\', '/'))
137 figure_node['width'] = img.size[0]
138 del img
139 elif figwidth is not None:
140 figure_node['width'] = figwidth
141 if figclasses:
142 figure_node['classes'] += figclasses
143 if align:
144 figure_node['align'] = align
145 if self.content:
146 node = nodes.Element() # anonymous container for parsing
147 self.state.nested_parse(self.content, self.content_offset, node)
148 first_node = node[0]
149 if isinstance(first_node, nodes.paragraph):
150 caption = nodes.caption(first_node.rawsource, '',
151 *first_node.children)
152 figure_node += caption
153 elif not (isinstance(first_node, nodes.comment)
154 and len(first_node) == 0):
155 error = self.state_machine.reporter.error(
156 'Figure caption must be a paragraph or empty comment.',
157 nodes.literal_block(self.block_text, self.block_text),
158 line=self.lineno)
159 return [figure_node, error]
160 if len(node) > 1:
161 figure_node += nodes.legend('', *node[1:])
162 return [figure_node]