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