images.py 6.6 KB

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