figmpl_directive.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. """
  2. Add a ``figure-mpl`` directive that is a responsive version of ``figure``.
  3. This implementation is very similar to ``.. figure::``, except it also allows a
  4. ``srcset=`` argument to be passed to the image tag, hence allowing responsive
  5. resolution images.
  6. There is no particular reason this could not be used standalone, but is meant
  7. to be used with :doc:`/api/sphinxext_plot_directive_api`.
  8. Note that the directory organization is a bit different than ``.. figure::``.
  9. See the *FigureMpl* documentation below.
  10. """
  11. from docutils import nodes
  12. from docutils.parsers.rst import directives
  13. from docutils.parsers.rst.directives.images import Figure, Image
  14. import os
  15. from os.path import relpath
  16. from pathlib import PurePath, Path
  17. import shutil
  18. from sphinx.errors import ExtensionError
  19. import matplotlib
  20. class figmplnode(nodes.General, nodes.Element):
  21. pass
  22. class FigureMpl(Figure):
  23. """
  24. Implements a directive to allow an optional hidpi image.
  25. Meant to be used with the *plot_srcset* configuration option in conf.py,
  26. and gets set in the TEMPLATE of plot_directive.py
  27. e.g.::
  28. .. figure-mpl:: plot_directive/some_plots-1.png
  29. :alt: bar
  30. :srcset: plot_directive/some_plots-1.png,
  31. plot_directive/some_plots-1.2x.png 2.00x
  32. :class: plot-directive
  33. The resulting html (at ``some_plots.html``) is::
  34. <img src="sphx_glr_bar_001_hidpi.png"
  35. srcset="_images/some_plot-1.png,
  36. _images/some_plots-1.2x.png 2.00x",
  37. alt="bar"
  38. class="plot_directive" />
  39. Note that the handling of subdirectories is different than that used by the sphinx
  40. figure directive::
  41. .. figure-mpl:: plot_directive/nestedpage/index-1.png
  42. :alt: bar
  43. :srcset: plot_directive/nestedpage/index-1.png
  44. plot_directive/nestedpage/index-1.2x.png 2.00x
  45. :class: plot_directive
  46. The resulting html (at ``nestedpage/index.html``)::
  47. <img src="../_images/nestedpage-index-1.png"
  48. srcset="../_images/nestedpage-index-1.png,
  49. ../_images/_images/nestedpage-index-1.2x.png 2.00x",
  50. alt="bar"
  51. class="sphx-glr-single-img" />
  52. where the subdirectory is included in the image name for uniqueness.
  53. """
  54. has_content = False
  55. required_arguments = 1
  56. optional_arguments = 2
  57. final_argument_whitespace = False
  58. option_spec = {
  59. 'alt': directives.unchanged,
  60. 'height': directives.length_or_unitless,
  61. 'width': directives.length_or_percentage_or_unitless,
  62. 'scale': directives.nonnegative_int,
  63. 'align': Image.align,
  64. 'class': directives.class_option,
  65. 'caption': directives.unchanged,
  66. 'srcset': directives.unchanged,
  67. }
  68. def run(self):
  69. image_node = figmplnode()
  70. imagenm = self.arguments[0]
  71. image_node['alt'] = self.options.get('alt', '')
  72. image_node['align'] = self.options.get('align', None)
  73. image_node['class'] = self.options.get('class', None)
  74. image_node['width'] = self.options.get('width', None)
  75. image_node['height'] = self.options.get('height', None)
  76. image_node['scale'] = self.options.get('scale', None)
  77. image_node['caption'] = self.options.get('caption', None)
  78. # we would like uri to be the highest dpi version so that
  79. # latex etc will use that. But for now, lets just make
  80. # imagenm... maybe pdf one day?
  81. image_node['uri'] = imagenm
  82. image_node['srcset'] = self.options.get('srcset', None)
  83. return [image_node]
  84. def _parse_srcsetNodes(st):
  85. """
  86. parse srcset...
  87. """
  88. entries = st.split(',')
  89. srcset = {}
  90. for entry in entries:
  91. spl = entry.strip().split(' ')
  92. if len(spl) == 1:
  93. srcset[0] = spl[0]
  94. elif len(spl) == 2:
  95. mult = spl[1][:-1]
  96. srcset[float(mult)] = spl[0]
  97. else:
  98. raise ExtensionError(f'srcset argument "{entry}" is invalid.')
  99. return srcset
  100. def _copy_images_figmpl(self, node):
  101. # these will be the temporary place the plot-directive put the images eg:
  102. # ../../../build/html/plot_directive/users/explain/artists/index-1.png
  103. if node['srcset']:
  104. srcset = _parse_srcsetNodes(node['srcset'])
  105. else:
  106. srcset = None
  107. # the rst file's location: eg /Users/username/matplotlib/doc/users/explain/artists
  108. docsource = PurePath(self.document['source']).parent
  109. # get the relpath relative to root:
  110. srctop = self.builder.srcdir
  111. rel = relpath(docsource, srctop).replace('.', '').replace(os.sep, '-')
  112. if len(rel):
  113. rel += '-'
  114. # eg: users/explain/artists
  115. imagedir = PurePath(self.builder.outdir, self.builder.imagedir)
  116. # eg: /Users/username/matplotlib/doc/build/html/_images/users/explain/artists
  117. Path(imagedir).mkdir(parents=True, exist_ok=True)
  118. # copy all the sources to the imagedir:
  119. if srcset:
  120. for src in srcset.values():
  121. # the entries in srcset are relative to docsource's directory
  122. abspath = PurePath(docsource, src)
  123. name = rel + abspath.name
  124. shutil.copyfile(abspath, imagedir / name)
  125. else:
  126. abspath = PurePath(docsource, node['uri'])
  127. name = rel + abspath.name
  128. shutil.copyfile(abspath, imagedir / name)
  129. return imagedir, srcset, rel
  130. def visit_figmpl_html(self, node):
  131. imagedir, srcset, rel = _copy_images_figmpl(self, node)
  132. # /doc/examples/subd/plot_1.rst
  133. docsource = PurePath(self.document['source'])
  134. # /doc/
  135. # make sure to add the trailing slash:
  136. srctop = PurePath(self.builder.srcdir, '')
  137. # examples/subd/plot_1.rst
  138. relsource = relpath(docsource, srctop)
  139. # /doc/build/html
  140. desttop = PurePath(self.builder.outdir, '')
  141. # /doc/build/html/examples/subd
  142. dest = desttop / relsource
  143. # ../../_images/ for dirhtml and ../_images/ for html
  144. imagerel = PurePath(relpath(imagedir, dest.parent)).as_posix()
  145. if self.builder.name == "dirhtml":
  146. imagerel = f'..{imagerel}'
  147. # make uri also be relative...
  148. nm = PurePath(node['uri'][1:]).name
  149. uri = f'{imagerel}/{rel}{nm}'
  150. # make srcset str. Need to change all the prefixes!
  151. maxsrc = uri
  152. srcsetst = ''
  153. if srcset:
  154. maxmult = -1
  155. for mult, src in srcset.items():
  156. nm = PurePath(src[1:]).name
  157. # ../../_images/plot_1_2_0x.png
  158. path = f'{imagerel}/{rel}{nm}'
  159. srcsetst += path
  160. if mult == 0:
  161. srcsetst += ', '
  162. else:
  163. srcsetst += f' {mult:1.2f}x, '
  164. if mult > maxmult:
  165. maxmult = mult
  166. maxsrc = path
  167. # trim trailing comma and space...
  168. srcsetst = srcsetst[:-2]
  169. alt = node['alt']
  170. if node['class'] is not None:
  171. classst = ' '.join(node['class'])
  172. classst = f'class="{classst}"'
  173. else:
  174. classst = ''
  175. stylers = ['width', 'height', 'scale']
  176. stylest = ''
  177. for style in stylers:
  178. if node[style]:
  179. stylest += f'{style}: {node[style]};'
  180. figalign = node['align'] if node['align'] else 'center'
  181. # <figure class="align-default" id="id1">
  182. # <a class="reference internal image-reference" href="_images/index-1.2x.png">
  183. # <img alt="_images/index-1.2x.png" src="_images/index-1.2x.png" style="width: 53%;" />
  184. # </a>
  185. # <figcaption>
  186. # <p><span class="caption-text">Figure caption is here....</span>
  187. # <a class="headerlink" href="#id1" title="Permalink to this image">#</a></p>
  188. # </figcaption>
  189. # </figure>
  190. img_block = (f'<img src="{uri}" style="{stylest}" srcset="{srcsetst}" '
  191. f'alt="{alt}" {classst}/>')
  192. html_block = f'<figure class="align-{figalign}">\n'
  193. html_block += f' <a class="reference internal image-reference" href="{maxsrc}">\n'
  194. html_block += f' {img_block}\n </a>\n'
  195. if node['caption']:
  196. html_block += ' <figcaption>\n'
  197. html_block += f' <p><span class="caption-text">{node["caption"]}</span></p>\n'
  198. html_block += ' </figcaption>\n'
  199. html_block += '</figure>\n'
  200. self.body.append(html_block)
  201. def visit_figmpl_latex(self, node):
  202. if node['srcset'] is not None:
  203. imagedir, srcset = _copy_images_figmpl(self, node)
  204. maxmult = -1
  205. # choose the highest res version for latex:
  206. maxmult = max(srcset, default=-1)
  207. node['uri'] = PurePath(srcset[maxmult]).name
  208. self.visit_figure(node)
  209. def depart_figmpl_html(self, node):
  210. pass
  211. def depart_figmpl_latex(self, node):
  212. self.depart_figure(node)
  213. def figurempl_addnode(app):
  214. app.add_node(figmplnode,
  215. html=(visit_figmpl_html, depart_figmpl_html),
  216. latex=(visit_figmpl_latex, depart_figmpl_latex))
  217. def setup(app):
  218. app.add_directive("figure-mpl", FigureMpl)
  219. figurempl_addnode(app)
  220. metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
  221. 'version': matplotlib.__version__}
  222. return metadata