treewide: remove redundant IS_ERR() before error code check
[linux/fpc-iii.git] / Documentation / sphinx / kfigure.py
blobfbfe6693bb60aa36157c60d33e6a2d0c1c630154
1 # -*- coding: utf-8; mode: python -*-
2 # pylint: disable=C0103, R0903, R0912, R0915
3 u"""
4 scalable figure and image handling
5 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7 Sphinx extension which implements scalable image handling.
9 :copyright: Copyright (C) 2016 Markus Heiser
10 :license: GPL Version 2, June 1991 see Linux/COPYING for details.
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.
19 * ``.. kernel-image``: for image handling / a ``.. image::`` replacement
21 * ``.. kernel-figure``: for figure handling / a ``.. figure::`` replacement
23 * ``.. kernel-render``: for render markup / a concept to embed *render*
24 markups (or languages). Supported markups (see ``RENDER_MARKUP_EXT``)
26 - ``DOT``: render embedded Graphviz's **DOC**
27 - ``SVG``: render embedded Scalable Vector Graphics (**SVG**)
28 - ... *developable*
30 Used tools:
32 * ``dot(1)``: Graphviz (http://www.graphviz.org). If Graphviz is not
33 available, the DOT language is inserted as literal-block.
35 * SVG to PDF: To generate PDF, you need at least one of this tools:
37 - ``convert(1)``: ImageMagick (https://www.imagemagick.org)
39 List of customizations:
41 * generate PDF from SVG / used by PDF (LaTeX) builder
43 * generate SVG (html-builder) and PDF (latex-builder) from DOT files.
44 DOT: see http://www.graphviz.org/content/dot-language
46 """
48 import os
49 from os import path
50 import subprocess
51 from hashlib import sha1
52 import sys
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
60 from sphinx.util.nodes import clean_astext
61 from six import iteritems
63 import kernellog
65 PY3 = sys.version_info[0] == 3
67 if PY3:
68 _unicode = str
69 else:
70 _unicode = unicode
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
80 __version__ = '1.0.0'
82 # simple helper
83 # -------------
85 def which(cmd):
86 """Searches the ``cmd`` in the ``PATH`` environment.
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
97 def mkdir(folder, mode=0o775):
98 if not path.isdir(folder):
99 os.makedirs(folder, mode)
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
107 def isNewer(path1, path2):
108 """Returns True if ``path1`` is newer than ``path2``
110 If ``path1`` exists and is newer than ``path2`` the function returns
111 ``True`` is returned otherwise ``False``
113 return (path.exists(path1)
114 and os.stat(path1).st_ctime > os.stat(path2).st_ctime)
116 def pass_handle(self, node): # pylint: disable=W0613
117 pass
119 # setup conversion tools and sphinx extension
120 # -------------------------------------------
122 # Graphviz's dot(1) support
123 dot_cmd = None
125 # ImageMagick' convert(1) support
126 convert_cmd = None
129 def setup(app):
130 # check toolchain first
131 app.connect('builder-inited', setupTools)
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), )
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), )
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), )
160 app.connect('doctree-read', add_kernel_figure_to_std_domain)
162 return dict(
163 version = __version__,
164 parallel_read_safe = True,
165 parallel_write_safe = True
169 def setupTools(app):
170 u"""
171 Check available build tools and log some *verbose* messages.
173 This function is called once, when the builder is initiated.
175 global dot_cmd, convert_cmd # pylint: disable=W0603
176 kernellog.verbose(app, "kfigure: check installed tools ...")
178 dot_cmd = which('dot')
179 convert_cmd = which('convert')
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)")
194 # integrate conversion tools
195 # --------------------------
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'
204 def convert_image(img_node, translator, src_fname=None):
205 """Convert a image node for the builder.
207 Different builder prefer different image formats, e.g. *latex* builder
208 prefer PDF while *html* builder prefer SVG format for images.
210 This function handles output image formats in dependence of source the
211 format (of the image) and the translator's output format.
213 app = translator.builder.app
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'])
221 dst_fname = None
223 # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
225 kernellog.verbose(app, 'assert best format for: ' + img_node['uri'])
227 if in_ext == '.dot':
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))
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'}
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')}
250 else:
251 # all other builder formats will include DOT as raw
252 img_node.replace_self(file2literal(src_fname))
254 elif in_ext == '.svg':
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'}
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:]
271 if isNewer(dst_fname, src_fname):
272 kernellog.verbose(app,
273 "convert: {out}/%s already exists and is newer" % _name)
275 else:
276 ok = False
277 mkdir(path.dirname(dst_fname))
279 if in_ext == '.dot':
280 kernellog.verbose(app, 'convert DOT to: {out}/' + _name)
281 ok = dot2format(app, src_fname, dst_fname)
283 elif in_ext == '.svg':
284 kernellog.verbose(app, 'convert SVG to: {out}/' + _name)
285 ok = svg2pdf(app, src_fname, dst_fname)
287 if not ok:
288 img_node.replace_self(file2literal(src_fname))
291 def dot2format(app, dot_fname, out_fname):
292 """Converts DOT file to ``out_fname`` using ``dot(1)``.
294 * ``dot_fname`` pathname of the input DOT file, including extension ``.dot``
295 * ``out_fname`` pathname of the output file, including format extension
297 The *format extension* depends on the ``dot`` command (see ``man dot``
298 option ``-Txxx``). Normally you will use one of the following extensions:
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.
306 out_format = path.splitext(out_fname)[1][1:]
307 cmd = [dot_cmd, '-T%s' % out_format, dot_fname]
308 exit_code = 42
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)
317 def svg2pdf(app, svg_fname, pdf_fname):
318 """Converts SVG to PDF with ``convert(1)`` command.
320 Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for
321 conversion. Returns ``True`` on success and ``False`` if an error occurred.
323 * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
324 * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``)
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)
335 # image handling
336 # ---------------------
338 def visit_kernel_image(self, node): # pylint: disable=W0613
339 """Visitor of the ``kernel_image`` Node.
341 Handles the ``image`` child-node with the ``convert_image(...)``.
343 img_node = node[0]
344 convert_image(img_node, self)
346 class kernel_image(nodes.image):
347 """Node for ``kernel-image`` directive."""
348 pass
350 class KernelImage(images.Image):
351 u"""KernelImage directive
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``.
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]
372 # figure handling
373 # ---------------------
375 def visit_kernel_figure(self, node): # pylint: disable=W0613
376 """Visitor of the ``kernel_figure`` Node.
378 Handles the ``image`` child-node with the ``convert_image(...)``.
380 img_node = node[0][0]
381 convert_image(img_node, self)
383 class kernel_figure(nodes.figure):
384 """Node for ``kernel-figure`` directive."""
386 class KernelFigure(Figure):
387 u"""KernelImage directive
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``.
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]
410 # render handling
411 # ---------------------
413 def visit_kernel_render(self, node):
414 """Visitor of the ``kernel_render`` Node.
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(...)``.
421 app = self.builder.app
422 srclang = node.get('srclang')
424 kernellog.verbose(app, 'visit kernel-render node lang: "%s"' % (srclang))
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
431 if not dot_cmd and tmp_ext == '.dot':
432 kernellog.verbose(app, "dot from graphviz not available / include raw.")
433 return
435 literal_block = node[0]
437 code = literal_block.astext()
438 hashobj = code.encode('utf-8') # str(node.attributes)
439 fname = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest()))
441 tmp_fname = path.join(
442 self.builder.outdir, self.builder.imagedir, fname + tmp_ext)
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)
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)}
454 literal_block.replace_self(img_node)
455 convert_image(img_node, self, tmp_fname)
458 class kernel_render(nodes.General, nodes.Inline, nodes.Element):
459 """Node for ``kernel-render`` directive."""
460 pass
462 class KernelRender(Figure):
463 u"""KernelRender directive
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.
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``.
474 has_content = True
475 required_arguments = 1
476 optional_arguments = 0
477 final_argument_whitespace = False
479 # earn options from 'figure'
480 option_spec = Figure.option_spec.copy()
481 option_spec['caption'] = directives.unchanged
483 def run(self):
484 return [self.build_node()]
486 def build_node(self):
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)]
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)]
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
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
519 figure_node = nodes.figure('', node)
520 for k,v in self.options.items():
521 figure_node[k] = v
522 figure_node += caption_node
524 node = figure_node
526 return node
528 def add_kernel_figure_to_std_domain(app, doctree):
529 """Add kernel-figure anchors to 'std' domain.
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.
536 This handle adds labels of kernel-figure to the 'std' domain labels.
539 std = app.env.domains["std"]
540 docname = app.env.docname
541 labels = std.data["labels"]
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]
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