Linux Audio

Check our new training course

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