kfigure.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. # -*- coding: utf-8; mode: python -*-
  2. # pylint: disable=C0103, R0903, R0912, R0915
  3. u"""
  4. scalable figure and image handling
  5. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  6. Sphinx extension which implements scalable image handling.
  7. :copyright: Copyright (C) 2016 Markus Heiser
  8. :license: GPL Version 2, June 1991 see Linux/COPYING for details.
  9. The build for image formats depend on image's source format and output's
  10. destination format. This extension implement methods to simplify image
  11. handling from the author's POV. Directives like ``kernel-figure`` implement
  12. methods *to* always get the best output-format even if some tools are not
  13. installed. For more details take a look at ``convert_image(...)`` which is
  14. the core of all conversions.
  15. * ``.. kernel-image``: for image handling / a ``.. image::`` replacement
  16. * ``.. kernel-figure``: for figure handling / a ``.. figure::`` replacement
  17. * ``.. kernel-render``: for render markup / a concept to embed *render*
  18. markups (or languages). Supported markups (see ``RENDER_MARKUP_EXT``)
  19. - ``DOT``: render embedded Graphviz's **DOC**
  20. - ``SVG``: render embedded Scalable Vector Graphics (**SVG**)
  21. - ... *developable*
  22. Used tools:
  23. * ``dot(1)``: Graphviz (http://www.graphviz.org). If Graphviz is not
  24. available, the DOT language is inserted as literal-block.
  25. * SVG to PDF: To generate PDF, you need at least one of this tools:
  26. - ``convert(1)``: ImageMagick (https://www.imagemagick.org)
  27. List of customizations:
  28. * generate PDF from SVG / used by PDF (LaTeX) builder
  29. * generate SVG (html-builder) and PDF (latex-builder) from DOT files.
  30. DOT: see http://www.graphviz.org/content/dot-language
  31. """
  32. import os
  33. from os import path
  34. import subprocess
  35. from hashlib import sha1
  36. import sys
  37. from docutils import nodes
  38. from docutils.statemachine import ViewList
  39. from docutils.parsers.rst import directives
  40. from docutils.parsers.rst.directives import images
  41. import sphinx
  42. from sphinx.util.nodes import clean_astext
  43. from six import iteritems
  44. import kernellog
  45. PY3 = sys.version_info[0] == 3
  46. if PY3:
  47. _unicode = str
  48. else:
  49. _unicode = unicode
  50. # Get Sphinx version
  51. major, minor, patch = sphinx.version_info[:3]
  52. if major == 1 and minor > 3:
  53. # patches.Figure only landed in Sphinx 1.4
  54. from sphinx.directives.patches import Figure # pylint: disable=C0413
  55. else:
  56. Figure = images.Figure
  57. __version__ = '1.0.0'
  58. # simple helper
  59. # -------------
  60. def which(cmd):
  61. """Searches the ``cmd`` in the ``PATH`` environment.
  62. This *which* searches the PATH for executable ``cmd`` . First match is
  63. returned, if nothing is found, ``None` is returned.
  64. """
  65. envpath = os.environ.get('PATH', None) or os.defpath
  66. for folder in envpath.split(os.pathsep):
  67. fname = folder + os.sep + cmd
  68. if path.isfile(fname):
  69. return fname
  70. def mkdir(folder, mode=0o775):
  71. if not path.isdir(folder):
  72. os.makedirs(folder, mode)
  73. def file2literal(fname):
  74. with open(fname, "r") as src:
  75. data = src.read()
  76. node = nodes.literal_block(data, data)
  77. return node
  78. def isNewer(path1, path2):
  79. """Returns True if ``path1`` is newer than ``path2``
  80. If ``path1`` exists and is newer than ``path2`` the function returns
  81. ``True`` is returned otherwise ``False``
  82. """
  83. return (path.exists(path1)
  84. and os.stat(path1).st_ctime > os.stat(path2).st_ctime)
  85. def pass_handle(self, node): # pylint: disable=W0613
  86. pass
  87. # setup conversion tools and sphinx extension
  88. # -------------------------------------------
  89. # Graphviz's dot(1) support
  90. dot_cmd = None
  91. # ImageMagick' convert(1) support
  92. convert_cmd = None
  93. def setup(app):
  94. # check toolchain first
  95. app.connect('builder-inited', setupTools)
  96. # image handling
  97. app.add_directive("kernel-image", KernelImage)
  98. app.add_node(kernel_image,
  99. html = (visit_kernel_image, pass_handle),
  100. latex = (visit_kernel_image, pass_handle),
  101. texinfo = (visit_kernel_image, pass_handle),
  102. text = (visit_kernel_image, pass_handle),
  103. man = (visit_kernel_image, pass_handle), )
  104. # figure handling
  105. app.add_directive("kernel-figure", KernelFigure)
  106. app.add_node(kernel_figure,
  107. html = (visit_kernel_figure, pass_handle),
  108. latex = (visit_kernel_figure, pass_handle),
  109. texinfo = (visit_kernel_figure, pass_handle),
  110. text = (visit_kernel_figure, pass_handle),
  111. man = (visit_kernel_figure, pass_handle), )
  112. # render handling
  113. app.add_directive('kernel-render', KernelRender)
  114. app.add_node(kernel_render,
  115. html = (visit_kernel_render, pass_handle),
  116. latex = (visit_kernel_render, pass_handle),
  117. texinfo = (visit_kernel_render, pass_handle),
  118. text = (visit_kernel_render, pass_handle),
  119. man = (visit_kernel_render, pass_handle), )
  120. app.connect('doctree-read', add_kernel_figure_to_std_domain)
  121. return dict(
  122. version = __version__,
  123. parallel_read_safe = True,
  124. parallel_write_safe = True
  125. )
  126. def setupTools(app):
  127. u"""
  128. Check available build tools and log some *verbose* messages.
  129. This function is called once, when the builder is initiated.
  130. """
  131. global dot_cmd, convert_cmd # pylint: disable=W0603
  132. kernellog.verbose(app, "kfigure: check installed tools ...")
  133. dot_cmd = which('dot')
  134. convert_cmd = which('convert')
  135. if dot_cmd:
  136. kernellog.verbose(app, "use dot(1) from: " + dot_cmd)
  137. else:
  138. kernellog.warn(app, "dot(1) not found, for better output quality install "
  139. "graphviz from http://www.graphviz.org")
  140. if convert_cmd:
  141. kernellog.verbose(app, "use convert(1) from: " + convert_cmd)
  142. else:
  143. kernellog.warn(app,
  144. "convert(1) not found, for SVG to PDF conversion install "
  145. "ImageMagick (https://www.imagemagick.org)")
  146. # integrate conversion tools
  147. # --------------------------
  148. RENDER_MARKUP_EXT = {
  149. # The '.ext' must be handled by convert_image(..) function's *in_ext* input.
  150. # <name> : <.ext>
  151. 'DOT' : '.dot',
  152. 'SVG' : '.svg'
  153. }
  154. def convert_image(img_node, translator, src_fname=None):
  155. """Convert a image node for the builder.
  156. Different builder prefer different image formats, e.g. *latex* builder
  157. prefer PDF while *html* builder prefer SVG format for images.
  158. This function handles output image formats in dependence of source the
  159. format (of the image) and the translator's output format.
  160. """
  161. app = translator.builder.app
  162. fname, in_ext = path.splitext(path.basename(img_node['uri']))
  163. if src_fname is None:
  164. src_fname = path.join(translator.builder.srcdir, img_node['uri'])
  165. if not path.exists(src_fname):
  166. src_fname = path.join(translator.builder.outdir, img_node['uri'])
  167. dst_fname = None
  168. # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
  169. kernellog.verbose(app, 'assert best format for: ' + img_node['uri'])
  170. if in_ext == '.dot':
  171. if not dot_cmd:
  172. kernellog.verbose(app,
  173. "dot from graphviz not available / include DOT raw.")
  174. img_node.replace_self(file2literal(src_fname))
  175. elif translator.builder.format == 'latex':
  176. dst_fname = path.join(translator.builder.outdir, fname + '.pdf')
  177. img_node['uri'] = fname + '.pdf'
  178. img_node['candidates'] = {'*': fname + '.pdf'}
  179. elif translator.builder.format == 'html':
  180. dst_fname = path.join(
  181. translator.builder.outdir,
  182. translator.builder.imagedir,
  183. fname + '.svg')
  184. img_node['uri'] = path.join(
  185. translator.builder.imgpath, fname + '.svg')
  186. img_node['candidates'] = {
  187. '*': path.join(translator.builder.imgpath, fname + '.svg')}
  188. else:
  189. # all other builder formats will include DOT as raw
  190. img_node.replace_self(file2literal(src_fname))
  191. elif in_ext == '.svg':
  192. if translator.builder.format == 'latex':
  193. if convert_cmd is None:
  194. kernellog.verbose(app,
  195. "no SVG to PDF conversion available / include SVG raw.")
  196. img_node.replace_self(file2literal(src_fname))
  197. else:
  198. dst_fname = path.join(translator.builder.outdir, fname + '.pdf')
  199. img_node['uri'] = fname + '.pdf'
  200. img_node['candidates'] = {'*': fname + '.pdf'}
  201. if dst_fname:
  202. # the builder needs not to copy one more time, so pop it if exists.
  203. translator.builder.images.pop(img_node['uri'], None)
  204. _name = dst_fname[len(translator.builder.outdir) + 1:]
  205. if isNewer(dst_fname, src_fname):
  206. kernellog.verbose(app,
  207. "convert: {out}/%s already exists and is newer" % _name)
  208. else:
  209. ok = False
  210. mkdir(path.dirname(dst_fname))
  211. if in_ext == '.dot':
  212. kernellog.verbose(app, 'convert DOT to: {out}/' + _name)
  213. ok = dot2format(app, src_fname, dst_fname)
  214. elif in_ext == '.svg':
  215. kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
  216. ok = svg2pdf(app, src_fname, dst_fname)
  217. if not ok:
  218. img_node.replace_self(file2literal(src_fname))
  219. def dot2format(app, dot_fname, out_fname):
  220. """Converts DOT file to ``out_fname`` using ``dot(1)``.
  221. * ``dot_fname`` pathname of the input DOT file, including extension ``.dot``
  222. * ``out_fname`` pathname of the output file, including format extension
  223. The *format extension* depends on the ``dot`` command (see ``man dot``
  224. option ``-Txxx``). Normally you will use one of the following extensions:
  225. - ``.ps`` for PostScript,
  226. - ``.svg`` or ``svgz`` for Structured Vector Graphics,
  227. - ``.fig`` for XFIG graphics and
  228. - ``.png`` or ``gif`` for common bitmap graphics.
  229. """
  230. out_format = path.splitext(out_fname)[1][1:]
  231. cmd = [dot_cmd, '-T%s' % out_format, dot_fname]
  232. exit_code = 42
  233. with open(out_fname, "w") as out:
  234. exit_code = subprocess.call(cmd, stdout = out)
  235. if exit_code != 0:
  236. kernellog.warn(app,
  237. "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
  238. return bool(exit_code == 0)
  239. def svg2pdf(app, svg_fname, pdf_fname):
  240. """Converts SVG to PDF with ``convert(1)`` command.
  241. Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for
  242. conversion. Returns ``True`` on success and ``False`` if an error occurred.
  243. * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
  244. * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``)
  245. """
  246. cmd = [convert_cmd, svg_fname, pdf_fname]
  247. # use stdout and stderr from parent
  248. exit_code = subprocess.call(cmd)
  249. if exit_code != 0:
  250. kernellog.warn(app, "Error #%d when calling: %s" % (exit_code, " ".join(cmd)))
  251. return bool(exit_code == 0)
  252. # image handling
  253. # ---------------------
  254. def visit_kernel_image(self, node): # pylint: disable=W0613
  255. """Visitor of the ``kernel_image`` Node.
  256. Handles the ``image`` child-node with the ``convert_image(...)``.
  257. """
  258. img_node = node[0]
  259. convert_image(img_node, self)
  260. class kernel_image(nodes.image):
  261. """Node for ``kernel-image`` directive."""
  262. pass
  263. class KernelImage(images.Image):
  264. u"""KernelImage directive
  265. Earns everything from ``.. image::`` directive, except *remote URI* and
  266. *glob* pattern. The KernelImage wraps a image node into a
  267. kernel_image node. See ``visit_kernel_image``.
  268. """
  269. def run(self):
  270. uri = self.arguments[0]
  271. if uri.endswith('.*') or uri.find('://') != -1:
  272. raise self.severe(
  273. 'Error in "%s: %s": glob pattern and remote images are not allowed'
  274. % (self.name, uri))
  275. result = images.Image.run(self)
  276. if len(result) == 2 or isinstance(result[0], nodes.system_message):
  277. return result
  278. (image_node,) = result
  279. # wrap image node into a kernel_image node / see visitors
  280. node = kernel_image('', image_node)
  281. return [node]
  282. # figure handling
  283. # ---------------------
  284. def visit_kernel_figure(self, node): # pylint: disable=W0613
  285. """Visitor of the ``kernel_figure`` Node.
  286. Handles the ``image`` child-node with the ``convert_image(...)``.
  287. """
  288. img_node = node[0][0]
  289. convert_image(img_node, self)
  290. class kernel_figure(nodes.figure):
  291. """Node for ``kernel-figure`` directive."""
  292. class KernelFigure(Figure):
  293. u"""KernelImage directive
  294. Earns everything from ``.. figure::`` directive, except *remote URI* and
  295. *glob* pattern. The KernelFigure wraps a figure node into a kernel_figure
  296. node. See ``visit_kernel_figure``.
  297. """
  298. def run(self):
  299. uri = self.arguments[0]
  300. if uri.endswith('.*') or uri.find('://') != -1:
  301. raise self.severe(
  302. 'Error in "%s: %s":'
  303. ' glob pattern and remote images are not allowed'
  304. % (self.name, uri))
  305. result = Figure.run(self)
  306. if len(result) == 2 or isinstance(result[0], nodes.system_message):
  307. return result
  308. (figure_node,) = result
  309. # wrap figure node into a kernel_figure node / see visitors
  310. node = kernel_figure('', figure_node)
  311. return [node]
  312. # render handling
  313. # ---------------------
  314. def visit_kernel_render(self, node):
  315. """Visitor of the ``kernel_render`` Node.
  316. If rendering tools available, save the markup of the ``literal_block`` child
  317. node into a file and replace the ``literal_block`` node with a new created
  318. ``image`` node, pointing to the saved markup file. Afterwards, handle the
  319. image child-node with the ``convert_image(...)``.
  320. """
  321. app = self.builder.app
  322. srclang = node.get('srclang')
  323. kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang))
  324. tmp_ext = RENDER_MARKUP_EXT.get(srclang, None)
  325. if tmp_ext is None:
  326. kernellog.warn(app, 'kernel-render: "%s" unknown / include raw.' % (srclang))
  327. return
  328. if not dot_cmd and tmp_ext == '.dot':
  329. kernellog.verbose(app, "dot from graphviz not available / include raw.")
  330. return
  331. literal_block = node[0]
  332. code = literal_block.astext()
  333. hashobj = code.encode('utf-8') # str(node.attributes)
  334. fname = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest()))
  335. tmp_fname = path.join(
  336. self.builder.outdir, self.builder.imagedir, fname + tmp_ext)
  337. if not path.isfile(tmp_fname):
  338. mkdir(path.dirname(tmp_fname))
  339. with open(tmp_fname, "w") as out:
  340. out.write(code)
  341. img_node = nodes.image(node.rawsource, **node.attributes)
  342. img_node['uri'] = path.join(self.builder.imgpath, fname + tmp_ext)
  343. img_node['candidates'] = {
  344. '*': path.join(self.builder.imgpath, fname + tmp_ext)}
  345. literal_block.replace_self(img_node)
  346. convert_image(img_node, self, tmp_fname)
  347. class kernel_render(nodes.General, nodes.Inline, nodes.Element):
  348. """Node for ``kernel-render`` directive."""
  349. pass
  350. class KernelRender(Figure):
  351. u"""KernelRender directive
  352. Render content by external tool. Has all the options known from the
  353. *figure* directive, plus option ``caption``. If ``caption`` has a
  354. value, a figure node with the *caption* is inserted. If not, a image node is
  355. inserted.
  356. The KernelRender directive wraps the text of the directive into a
  357. literal_block node and wraps it into a kernel_render node. See
  358. ``visit_kernel_render``.
  359. """
  360. has_content = True
  361. required_arguments = 1
  362. optional_arguments = 0
  363. final_argument_whitespace = False
  364. # earn options from 'figure'
  365. option_spec = Figure.option_spec.copy()
  366. option_spec['caption'] = directives.unchanged
  367. def run(self):
  368. return [self.build_node()]
  369. def build_node(self):
  370. srclang = self.arguments[0].strip()
  371. if srclang not in RENDER_MARKUP_EXT.keys():
  372. return [self.state_machine.reporter.warning(
  373. 'Unknown source language "%s", use one of: %s.' % (
  374. srclang, ",".join(RENDER_MARKUP_EXT.keys())),
  375. line=self.lineno)]
  376. code = '\n'.join(self.content)
  377. if not code.strip():
  378. return [self.state_machine.reporter.warning(
  379. 'Ignoring "%s" directive without content.' % (
  380. self.name),
  381. line=self.lineno)]
  382. node = kernel_render()
  383. node['alt'] = self.options.get('alt','')
  384. node['srclang'] = srclang
  385. literal_node = nodes.literal_block(code, code)
  386. node += literal_node
  387. caption = self.options.get('caption')
  388. if caption:
  389. # parse caption's content
  390. parsed = nodes.Element()
  391. self.state.nested_parse(
  392. ViewList([caption], source=''), self.content_offset, parsed)
  393. caption_node = nodes.caption(
  394. parsed[0].rawsource, '', *parsed[0].children)
  395. caption_node.source = parsed[0].source
  396. caption_node.line = parsed[0].line
  397. figure_node = nodes.figure('', node)
  398. for k,v in self.options.items():
  399. figure_node[k] = v
  400. figure_node += caption_node
  401. node = figure_node
  402. return node
  403. def add_kernel_figure_to_std_domain(app, doctree):
  404. """Add kernel-figure anchors to 'std' domain.
  405. The ``StandardDomain.process_doc(..)`` method does not know how to resolve
  406. the caption (label) of ``kernel-figure`` directive (it only knows about
  407. standard nodes, e.g. table, figure etc.). Without any additional handling
  408. this will result in a 'undefined label' for kernel-figures.
  409. This handle adds labels of kernel-figure to the 'std' domain labels.
  410. """
  411. std = app.env.domains["std"]
  412. docname = app.env.docname
  413. labels = std.data["labels"]
  414. for name, explicit in iteritems(doctree.nametypes):
  415. if not explicit:
  416. continue
  417. labelid = doctree.nameids[name]
  418. if labelid is None:
  419. continue
  420. node = doctree.ids[labelid]
  421. if node.tagname == 'kernel_figure':
  422. for n in node.next_node():
  423. if n.tagname == 'caption':
  424. sectname = clean_astext(n)
  425. # add label to std domain
  426. labels[name] = docname, labelid, sectname
  427. break