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