Loading...
1# -*- coding: utf-8; mode: python -*-
2# pylint: disable=C0103, R0903, R0912, R0915
3u"""
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
48import os
49from os import path
50import subprocess
51from hashlib import sha1
52import sys
53
54from docutils import nodes
55from docutils.statemachine import ViewList
56from docutils.parsers.rst import directives
57from docutils.parsers.rst.directives import images
58import sphinx
59
60from sphinx.util.nodes import clean_astext
61from six import iteritems
62
63PY3 = sys.version_info[0] == 3
64
65if PY3:
66 _unicode = str
67else:
68 _unicode = unicode
69
70# Get Sphinx version
71major, minor, patch = sphinx.version_info[:3]
72if major == 1 and minor > 3:
73 # patches.Figure only landed in Sphinx 1.4
74 from sphinx.directives.patches import Figure # pylint: disable=C0413
75else:
76 Figure = images.Figure
77
78__version__ = '1.0.0'
79
80# simple helper
81# -------------
82
83def which(cmd):
84 """Searches the ``cmd`` in the ``PATH`` environment.
85
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
94
95def mkdir(folder, mode=0o775):
96 if not path.isdir(folder):
97 os.makedirs(folder, mode)
98
99def file2literal(fname):
100 with open(fname, "r") as src:
101 data = src.read()
102 node = nodes.literal_block(data, data)
103 return node
104
105def isNewer(path1, path2):
106 """Returns True if ``path1`` is newer than ``path2``
107
108 If ``path1`` exists and is newer than ``path2`` the function returns
109 ``True`` is returned otherwise ``False``
110 """
111 return (path.exists(path1)
112 and os.stat(path1).st_ctime > os.stat(path2).st_ctime)
113
114def pass_handle(self, node): # pylint: disable=W0613
115 pass
116
117# setup conversion tools and sphinx extension
118# -------------------------------------------
119
120# Graphviz's dot(1) support
121dot_cmd = None
122
123# ImageMagick' convert(1) support
124convert_cmd = None
125
126
127def setup(app):
128 # check toolchain first
129 app.connect('builder-inited', setupTools)
130
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), )
139
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), )
148
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), )
157
158 app.connect('doctree-read', add_kernel_figure_to_std_domain)
159
160 return dict(
161 version = __version__,
162 parallel_read_safe = True,
163 parallel_write_safe = True
164 )
165
166
167def setupTools(app):
168 u"""
169 Check available build tools and log some *verbose* messages.
170
171 This function is called once, when the builder is initiated.
172 """
173 global dot_cmd, convert_cmd # pylint: disable=W0603
174 app.verbose("kfigure: check installed tools ...")
175
176 dot_cmd = which('dot')
177 convert_cmd = which('convert')
178
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)")
190
191
192# integrate conversion tools
193# --------------------------
194
195RENDER_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'
200}
201
202def convert_image(img_node, translator, src_fname=None):
203 """Convert a image node for the builder.
204
205 Different builder prefer different image formats, e.g. *latex* builder
206 prefer PDF while *html* builder prefer SVG format for images.
207
208 This function handles output image formats in dependence of source the
209 format (of the image) and the translator's output format.
210 """
211 app = translator.builder.app
212
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'])
218
219 dst_fname = None
220
221 # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages
222
223 app.verbose('assert best format for: ' + img_node['uri'])
224
225 if in_ext == '.dot':
226
227 if not dot_cmd:
228 app.verbose("dot from graphviz not available / include DOT raw.")
229 img_node.replace_self(file2literal(src_fname))
230
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'}
235
236
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')}
246
247 else:
248 # all other builder formats will include DOT as raw
249 img_node.replace_self(file2literal(src_fname))
250
251 elif in_ext == '.svg':
252
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'}
261
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:]
266
267 if isNewer(dst_fname, src_fname):
268 app.verbose("convert: {out}/%s already exists and is newer" % _name)
269
270 else:
271 ok = False
272 mkdir(path.dirname(dst_fname))
273
274 if in_ext == '.dot':
275 app.verbose('convert DOT to: {out}/' + _name)
276 ok = dot2format(app, src_fname, dst_fname)
277
278 elif in_ext == '.svg':
279 app.verbose('convert SVG to: {out}/' + _name)
280 ok = svg2pdf(app, src_fname, dst_fname)
281
282 if not ok:
283 img_node.replace_self(file2literal(src_fname))
284
285
286def dot2format(app, dot_fname, out_fname):
287 """Converts DOT file to ``out_fname`` using ``dot(1)``.
288
289 * ``dot_fname`` pathname of the input DOT file, including extension ``.dot``
290 * ``out_fname`` pathname of the output file, including format extension
291
292 The *format extension* depends on the ``dot`` command (see ``man dot``
293 option ``-Txxx``). Normally you will use one of the following extensions:
294
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.
299
300 """
301 out_format = path.splitext(out_fname)[1][1:]
302 cmd = [dot_cmd, '-T%s' % out_format, dot_fname]
303 exit_code = 42
304
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)
310
311def svg2pdf(app, svg_fname, pdf_fname):
312 """Converts SVG to PDF with ``convert(1)`` command.
313
314 Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for
315 conversion. Returns ``True`` on success and ``False`` if an error occurred.
316
317 * ``svg_fname`` pathname of the input SVG file with extension (``.svg``)
318 * ``pdf_name`` pathname of the output PDF file with extension (``.pdf``)
319
320 """
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)
327
328
329# image handling
330# ---------------------
331
332def visit_kernel_image(self, node): # pylint: disable=W0613
333 """Visitor of the ``kernel_image`` Node.
334
335 Handles the ``image`` child-node with the ``convert_image(...)``.
336 """
337 img_node = node[0]
338 convert_image(img_node, self)
339
340class kernel_image(nodes.image):
341 """Node for ``kernel-image`` directive."""
342 pass
343
344class KernelImage(images.Image):
345 u"""KernelImage directive
346
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``.
350 """
351
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]
365
366# figure handling
367# ---------------------
368
369def visit_kernel_figure(self, node): # pylint: disable=W0613
370 """Visitor of the ``kernel_figure`` Node.
371
372 Handles the ``image`` child-node with the ``convert_image(...)``.
373 """
374 img_node = node[0][0]
375 convert_image(img_node, self)
376
377class kernel_figure(nodes.figure):
378 """Node for ``kernel-figure`` directive."""
379
380class KernelFigure(Figure):
381 u"""KernelImage directive
382
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``.
386 """
387
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]
402
403
404# render handling
405# ---------------------
406
407def visit_kernel_render(self, node):
408 """Visitor of the ``kernel_render`` Node.
409
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(...)``.
414 """
415 app = self.builder.app
416 srclang = node.get('srclang')
417
418 app.verbose('visit kernel-render node lang: "%s"' % (srclang))
419
420 tmp_ext = RENDER_MARKUP_EXT.get(srclang, None)
421 if tmp_ext is None:
422 app.warn('kernel-render: "%s" unknown / include raw.' % (srclang))
423 return
424
425 if not dot_cmd and tmp_ext == '.dot':
426 app.verbose("dot from graphviz not available / include raw.")
427 return
428
429 literal_block = node[0]
430
431 code = literal_block.astext()
432 hashobj = code.encode('utf-8') # str(node.attributes)
433 fname = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest()))
434
435 tmp_fname = path.join(
436 self.builder.outdir, self.builder.imagedir, fname + tmp_ext)
437
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)
442
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)}
447
448 literal_block.replace_self(img_node)
449 convert_image(img_node, self, tmp_fname)
450
451
452class kernel_render(nodes.General, nodes.Inline, nodes.Element):
453 """Node for ``kernel-render`` directive."""
454 pass
455
456class KernelRender(Figure):
457 u"""KernelRender directive
458
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.
463
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``.
467 """
468 has_content = True
469 required_arguments = 1
470 optional_arguments = 0
471 final_argument_whitespace = False
472
473 # earn options from 'figure'
474 option_spec = Figure.option_spec.copy()
475 option_spec['caption'] = directives.unchanged
476
477 def run(self):
478 return [self.build_node()]
479
480 def build_node(self):
481
482 srclang = self.arguments[0].strip()
483 if srclang not in RENDER_MARKUP_EXT.keys():
484 return [self.state_machine.reporter.warning(
485 'Unknown source language "%s", use one of: %s.' % (
486 srclang, ",".join(RENDER_MARKUP_EXT.keys())),
487 line=self.lineno)]
488
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)]
495
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
501
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
512
513 figure_node = nodes.figure('', node)
514 for k,v in self.options.items():
515 figure_node[k] = v
516 figure_node += caption_node
517
518 node = figure_node
519
520 return node
521
522def add_kernel_figure_to_std_domain(app, doctree):
523 """Add kernel-figure anchors to 'std' domain.
524
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.
529
530 This handle adds labels of kernel-figure to the 'std' domain labels.
531 """
532
533 std = app.env.domains["std"]
534 docname = app.env.docname
535 labels = std.data["labels"]
536
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]
544
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
1# -*- coding: utf-8; mode: python -*-
2# pylint: disable=C0103, R0903, R0912, R0915
3u"""
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
48import os
49from os import path
50import subprocess
51from hashlib import sha1
52from docutils import nodes
53from docutils.statemachine import ViewList
54from docutils.parsers.rst import directives
55from docutils.parsers.rst.directives import images
56import sphinx
57from sphinx.util.nodes import clean_astext
58import kernellog
59
60# Get Sphinx version
61major, minor, patch = sphinx.version_info[:3]
62if major == 1 and minor > 3:
63 # patches.Figure only landed in Sphinx 1.4
64 from sphinx.directives.patches import Figure # pylint: disable=C0413
65else:
66 Figure = images.Figure
67
68__version__ = '1.0.0'
69
70# simple helper
71# -------------
72
73def 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
85def mkdir(folder, mode=0o775):
86 if not path.isdir(folder):
87 os.makedirs(folder, mode)
88
89def file2literal(fname):
90 with open(fname, "r") as src:
91 data = src.read()
92 node = nodes.literal_block(data, data)
93 return node
94
95def 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
104def 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
111dot_cmd = None
112
113# ImageMagick' convert(1) support
114convert_cmd = None
115
116
117def 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
157def 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
185RENDER_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
192def 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
279def 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
305def 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
326def 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
334class kernel_image(nodes.image):
335 """Node for ``kernel-image`` directive."""
336 pass
337
338class 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
363def 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
371class kernel_figure(nodes.figure):
372 """Node for ``kernel-figure`` directive."""
373
374class 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
401def 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
446class kernel_render(nodes.General, nodes.Inline, nodes.Element):
447 """Node for ``kernel-render`` directive."""
448 pass
449
450class 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
516def 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