]> git.proxmox.com Git - mirror_ubuntu-jammy-kernel.git/blob - Documentation/sphinx/kfigure.py
Merge tag 'media/v5.6-1' of git://git.kernel.org/pub/scm/linux/kernel/git/mchehab...
[mirror_ubuntu-jammy-kernel.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 (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