linux/Documentation/sphinx/kfigure.py
<<
>>
Prefs
   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
 546