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