|  | # -*- coding: utf-8; mode: python -*- | 
|  | # pylint: disable=C0103, R0903, R0912, R0915 | 
|  | u""" | 
|  | scalable figure and image handling | 
|  | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | 
|  |  | 
|  | Sphinx extension which implements scalable image handling. | 
|  |  | 
|  | :copyright:  Copyright (C) 2016  Markus Heiser | 
|  | :license:    GPL Version 2, June 1991 see Linux/COPYING for details. | 
|  |  | 
|  | The build for image formats depend on image's source format and output's | 
|  | destination format. This extension implement methods to simplify image | 
|  | handling from the author's POV. Directives like ``kernel-figure`` implement | 
|  | methods *to* always get the best output-format even if some tools are not | 
|  | installed. For more details take a look at ``convert_image(...)`` which is | 
|  | the core of all conversions. | 
|  |  | 
|  | * ``.. kernel-image``: for image handling / a ``.. image::`` replacement | 
|  |  | 
|  | * ``.. kernel-figure``: for figure handling / a ``.. figure::`` replacement | 
|  |  | 
|  | * ``.. kernel-render``: for render markup / a concept to embed *render* | 
|  | markups (or languages). Supported markups (see ``RENDER_MARKUP_EXT``) | 
|  |  | 
|  | - ``DOT``: render embedded Graphviz's **DOC** | 
|  | - ``SVG``: render embedded Scalable Vector Graphics (**SVG**) | 
|  | - ... *developable* | 
|  |  | 
|  | Used tools: | 
|  |  | 
|  | * ``dot(1)``: Graphviz (http://www.graphviz.org). If Graphviz is not | 
|  | available, the DOT language is inserted as literal-block. | 
|  |  | 
|  | * SVG to PDF: To generate PDF, you need at least one of this tools: | 
|  |  | 
|  | - ``convert(1)``: ImageMagick (https://www.imagemagick.org) | 
|  |  | 
|  | List of customizations: | 
|  |  | 
|  | * generate PDF from SVG / used by PDF (LaTeX) builder | 
|  |  | 
|  | * generate SVG (html-builder) and PDF (latex-builder) from DOT files. | 
|  | DOT: see http://www.graphviz.org/content/dot-language | 
|  |  | 
|  | """ | 
|  |  | 
|  | import os | 
|  | from os import path | 
|  | import subprocess | 
|  | from hashlib import sha1 | 
|  | import sys | 
|  |  | 
|  | from docutils import nodes | 
|  | from docutils.statemachine import ViewList | 
|  | from docutils.parsers.rst import directives | 
|  | from docutils.parsers.rst.directives import images | 
|  | import sphinx | 
|  |  | 
|  | from sphinx.util.nodes import clean_astext | 
|  | from six import iteritems | 
|  |  | 
|  | PY3 = sys.version_info[0] == 3 | 
|  |  | 
|  | if PY3: | 
|  | _unicode = str | 
|  | else: | 
|  | _unicode = unicode | 
|  |  | 
|  | # Get Sphinx version | 
|  | major, minor, patch = sphinx.version_info[:3] | 
|  | if major == 1 and minor > 3: | 
|  | # patches.Figure only landed in Sphinx 1.4 | 
|  | from sphinx.directives.patches import Figure  # pylint: disable=C0413 | 
|  | else: | 
|  | Figure = images.Figure | 
|  |  | 
|  | __version__  = '1.0.0' | 
|  |  | 
|  | # simple helper | 
|  | # ------------- | 
|  |  | 
|  | def which(cmd): | 
|  | """Searches the ``cmd`` in the ``PATH`` environment. | 
|  |  | 
|  | This *which* searches the PATH for executable ``cmd`` . First match is | 
|  | returned, if nothing is found, ``None` is returned. | 
|  | """ | 
|  | envpath = os.environ.get('PATH', None) or os.defpath | 
|  | for folder in envpath.split(os.pathsep): | 
|  | fname = folder + os.sep + cmd | 
|  | if path.isfile(fname): | 
|  | return fname | 
|  |  | 
|  | def mkdir(folder, mode=0o775): | 
|  | if not path.isdir(folder): | 
|  | os.makedirs(folder, mode) | 
|  |  | 
|  | def file2literal(fname): | 
|  | with open(fname, "r") as src: | 
|  | data = src.read() | 
|  | node = nodes.literal_block(data, data) | 
|  | return node | 
|  |  | 
|  | def isNewer(path1, path2): | 
|  | """Returns True if ``path1`` is newer than ``path2`` | 
|  |  | 
|  | If ``path1`` exists and is newer than ``path2`` the function returns | 
|  | ``True`` is returned otherwise ``False`` | 
|  | """ | 
|  | return (path.exists(path1) | 
|  | and os.stat(path1).st_ctime > os.stat(path2).st_ctime) | 
|  |  | 
|  | def pass_handle(self, node):           # pylint: disable=W0613 | 
|  | pass | 
|  |  | 
|  | # setup conversion tools and sphinx extension | 
|  | # ------------------------------------------- | 
|  |  | 
|  | # Graphviz's dot(1) support | 
|  | dot_cmd = None | 
|  |  | 
|  | # ImageMagick' convert(1) support | 
|  | convert_cmd = None | 
|  |  | 
|  |  | 
|  | def setup(app): | 
|  | # check toolchain first | 
|  | app.connect('builder-inited', setupTools) | 
|  |  | 
|  | # image handling | 
|  | app.add_directive("kernel-image",  KernelImage) | 
|  | app.add_node(kernel_image, | 
|  | html    = (visit_kernel_image, pass_handle), | 
|  | latex   = (visit_kernel_image, pass_handle), | 
|  | texinfo = (visit_kernel_image, pass_handle), | 
|  | text    = (visit_kernel_image, pass_handle), | 
|  | man     = (visit_kernel_image, pass_handle), ) | 
|  |  | 
|  | # figure handling | 
|  | app.add_directive("kernel-figure", KernelFigure) | 
|  | app.add_node(kernel_figure, | 
|  | html    = (visit_kernel_figure, pass_handle), | 
|  | latex   = (visit_kernel_figure, pass_handle), | 
|  | texinfo = (visit_kernel_figure, pass_handle), | 
|  | text    = (visit_kernel_figure, pass_handle), | 
|  | man     = (visit_kernel_figure, pass_handle), ) | 
|  |  | 
|  | # render handling | 
|  | app.add_directive('kernel-render', KernelRender) | 
|  | app.add_node(kernel_render, | 
|  | html    = (visit_kernel_render, pass_handle), | 
|  | latex   = (visit_kernel_render, pass_handle), | 
|  | texinfo = (visit_kernel_render, pass_handle), | 
|  | text    = (visit_kernel_render, pass_handle), | 
|  | man     = (visit_kernel_render, pass_handle), ) | 
|  |  | 
|  | app.connect('doctree-read', add_kernel_figure_to_std_domain) | 
|  |  | 
|  | return dict( | 
|  | version = __version__, | 
|  | parallel_read_safe = True, | 
|  | parallel_write_safe = True | 
|  | ) | 
|  |  | 
|  |  | 
|  | def setupTools(app): | 
|  | u""" | 
|  | Check available build tools and log some *verbose* messages. | 
|  |  | 
|  | This function is called once, when the builder is initiated. | 
|  | """ | 
|  | global dot_cmd, convert_cmd   # pylint: disable=W0603 | 
|  | app.verbose("kfigure: check installed tools ...") | 
|  |  | 
|  | dot_cmd = which('dot') | 
|  | convert_cmd = which('convert') | 
|  |  | 
|  | if dot_cmd: | 
|  | app.verbose("use dot(1) from: " + dot_cmd) | 
|  | else: | 
|  | app.warn("dot(1) not found, for better output quality install " | 
|  | "graphviz from http://www.graphviz.org") | 
|  | if convert_cmd: | 
|  | app.verbose("use convert(1) from: " + convert_cmd) | 
|  | else: | 
|  | app.warn( | 
|  | "convert(1) not found, for SVG to PDF conversion install " | 
|  | "ImageMagick (https://www.imagemagick.org)") | 
|  |  | 
|  |  | 
|  | # integrate conversion tools | 
|  | # -------------------------- | 
|  |  | 
|  | RENDER_MARKUP_EXT = { | 
|  | # The '.ext' must be handled by convert_image(..) function's *in_ext* input. | 
|  | # <name> : <.ext> | 
|  | 'DOT' : '.dot', | 
|  | 'SVG' : '.svg' | 
|  | } | 
|  |  | 
|  | def convert_image(img_node, translator, src_fname=None): | 
|  | """Convert a image node for the builder. | 
|  |  | 
|  | Different builder prefer different image formats, e.g. *latex* builder | 
|  | prefer PDF while *html* builder prefer SVG format for images. | 
|  |  | 
|  | This function handles output image formats in dependence of source the | 
|  | format (of the image) and the translator's output format. | 
|  | """ | 
|  | app = translator.builder.app | 
|  |  | 
|  | fname, in_ext = path.splitext(path.basename(img_node['uri'])) | 
|  | if src_fname is None: | 
|  | src_fname = path.join(translator.builder.srcdir, img_node['uri']) | 
|  | if not path.exists(src_fname): | 
|  | src_fname = path.join(translator.builder.outdir, img_node['uri']) | 
|  |  | 
|  | dst_fname = None | 
|  |  | 
|  | # in kernel builds, use 'make SPHINXOPTS=-v' to see verbose messages | 
|  |  | 
|  | app.verbose('assert best format for: ' + img_node['uri']) | 
|  |  | 
|  | if in_ext == '.dot': | 
|  |  | 
|  | if not dot_cmd: | 
|  | app.verbose("dot from graphviz not available / include DOT raw.") | 
|  | img_node.replace_self(file2literal(src_fname)) | 
|  |  | 
|  | elif translator.builder.format == 'latex': | 
|  | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') | 
|  | img_node['uri'] = fname + '.pdf' | 
|  | img_node['candidates'] = {'*': fname + '.pdf'} | 
|  |  | 
|  |  | 
|  | elif translator.builder.format == 'html': | 
|  | dst_fname = path.join( | 
|  | translator.builder.outdir, | 
|  | translator.builder.imagedir, | 
|  | fname + '.svg') | 
|  | img_node['uri'] = path.join( | 
|  | translator.builder.imgpath, fname + '.svg') | 
|  | img_node['candidates'] = { | 
|  | '*': path.join(translator.builder.imgpath, fname + '.svg')} | 
|  |  | 
|  | else: | 
|  | # all other builder formats will include DOT as raw | 
|  | img_node.replace_self(file2literal(src_fname)) | 
|  |  | 
|  | elif in_ext == '.svg': | 
|  |  | 
|  | if translator.builder.format == 'latex': | 
|  | if convert_cmd is None: | 
|  | app.verbose("no SVG to PDF conversion available / include SVG raw.") | 
|  | img_node.replace_self(file2literal(src_fname)) | 
|  | else: | 
|  | dst_fname = path.join(translator.builder.outdir, fname + '.pdf') | 
|  | img_node['uri'] = fname + '.pdf' | 
|  | img_node['candidates'] = {'*': fname + '.pdf'} | 
|  |  | 
|  | if dst_fname: | 
|  | # the builder needs not to copy one more time, so pop it if exists. | 
|  | translator.builder.images.pop(img_node['uri'], None) | 
|  | _name = dst_fname[len(translator.builder.outdir) + 1:] | 
|  |  | 
|  | if isNewer(dst_fname, src_fname): | 
|  | app.verbose("convert: {out}/%s already exists and is newer" % _name) | 
|  |  | 
|  | else: | 
|  | ok = False | 
|  | mkdir(path.dirname(dst_fname)) | 
|  |  | 
|  | if in_ext == '.dot': | 
|  | app.verbose('convert DOT to: {out}/' + _name) | 
|  | ok = dot2format(app, src_fname, dst_fname) | 
|  |  | 
|  | elif in_ext == '.svg': | 
|  | app.verbose('convert SVG to: {out}/' + _name) | 
|  | ok = svg2pdf(app, src_fname, dst_fname) | 
|  |  | 
|  | if not ok: | 
|  | img_node.replace_self(file2literal(src_fname)) | 
|  |  | 
|  |  | 
|  | def dot2format(app, dot_fname, out_fname): | 
|  | """Converts DOT file to ``out_fname`` using ``dot(1)``. | 
|  |  | 
|  | * ``dot_fname`` pathname of the input DOT file, including extension ``.dot`` | 
|  | * ``out_fname`` pathname of the output file, including format extension | 
|  |  | 
|  | The *format extension* depends on the ``dot`` command (see ``man dot`` | 
|  | option ``-Txxx``). Normally you will use one of the following extensions: | 
|  |  | 
|  | - ``.ps`` for PostScript, | 
|  | - ``.svg`` or ``svgz`` for Structured Vector Graphics, | 
|  | - ``.fig`` for XFIG graphics and | 
|  | - ``.png`` or ``gif`` for common bitmap graphics. | 
|  |  | 
|  | """ | 
|  | out_format = path.splitext(out_fname)[1][1:] | 
|  | cmd = [dot_cmd, '-T%s' % out_format, dot_fname] | 
|  | exit_code = 42 | 
|  |  | 
|  | with open(out_fname, "w") as out: | 
|  | exit_code = subprocess.call(cmd, stdout = out) | 
|  | if exit_code != 0: | 
|  | app.warn("Error #%d when calling: %s" % (exit_code, " ".join(cmd))) | 
|  | return bool(exit_code == 0) | 
|  |  | 
|  | def svg2pdf(app, svg_fname, pdf_fname): | 
|  | """Converts SVG to PDF with ``convert(1)`` command. | 
|  |  | 
|  | Uses ``convert(1)`` from ImageMagick (https://www.imagemagick.org) for | 
|  | conversion.  Returns ``True`` on success and ``False`` if an error occurred. | 
|  |  | 
|  | * ``svg_fname`` pathname of the input SVG file with extension (``.svg``) | 
|  | * ``pdf_name``  pathname of the output PDF file with extension (``.pdf``) | 
|  |  | 
|  | """ | 
|  | cmd = [convert_cmd, svg_fname, pdf_fname] | 
|  | # use stdout and stderr from parent | 
|  | exit_code = subprocess.call(cmd) | 
|  | if exit_code != 0: | 
|  | app.warn("Error #%d when calling: %s" % (exit_code, " ".join(cmd))) | 
|  | return bool(exit_code == 0) | 
|  |  | 
|  |  | 
|  | # image handling | 
|  | # --------------------- | 
|  |  | 
|  | def visit_kernel_image(self, node):    # pylint: disable=W0613 | 
|  | """Visitor of the ``kernel_image`` Node. | 
|  |  | 
|  | Handles the ``image`` child-node with the ``convert_image(...)``. | 
|  | """ | 
|  | img_node = node[0] | 
|  | convert_image(img_node, self) | 
|  |  | 
|  | class kernel_image(nodes.image): | 
|  | """Node for ``kernel-image`` directive.""" | 
|  | pass | 
|  |  | 
|  | class KernelImage(images.Image): | 
|  | u"""KernelImage directive | 
|  |  | 
|  | Earns everything from ``.. image::`` directive, except *remote URI* and | 
|  | *glob* pattern. The KernelImage wraps a image node into a | 
|  | kernel_image node. See ``visit_kernel_image``. | 
|  | """ | 
|  |  | 
|  | def run(self): | 
|  | uri = self.arguments[0] | 
|  | if uri.endswith('.*') or uri.find('://') != -1: | 
|  | raise self.severe( | 
|  | 'Error in "%s: %s": glob pattern and remote images are not allowed' | 
|  | % (self.name, uri)) | 
|  | result = images.Image.run(self) | 
|  | if len(result) == 2 or isinstance(result[0], nodes.system_message): | 
|  | return result | 
|  | (image_node,) = result | 
|  | # wrap image node into a kernel_image node / see visitors | 
|  | node = kernel_image('', image_node) | 
|  | return [node] | 
|  |  | 
|  | # figure handling | 
|  | # --------------------- | 
|  |  | 
|  | def visit_kernel_figure(self, node):   # pylint: disable=W0613 | 
|  | """Visitor of the ``kernel_figure`` Node. | 
|  |  | 
|  | Handles the ``image`` child-node with the ``convert_image(...)``. | 
|  | """ | 
|  | img_node = node[0][0] | 
|  | convert_image(img_node, self) | 
|  |  | 
|  | class kernel_figure(nodes.figure): | 
|  | """Node for ``kernel-figure`` directive.""" | 
|  |  | 
|  | class KernelFigure(Figure): | 
|  | u"""KernelImage directive | 
|  |  | 
|  | Earns everything from ``.. figure::`` directive, except *remote URI* and | 
|  | *glob* pattern.  The KernelFigure wraps a figure node into a kernel_figure | 
|  | node. See ``visit_kernel_figure``. | 
|  | """ | 
|  |  | 
|  | def run(self): | 
|  | uri = self.arguments[0] | 
|  | if uri.endswith('.*') or uri.find('://') != -1: | 
|  | raise self.severe( | 
|  | 'Error in "%s: %s":' | 
|  | ' glob pattern and remote images are not allowed' | 
|  | % (self.name, uri)) | 
|  | result = Figure.run(self) | 
|  | if len(result) == 2 or isinstance(result[0], nodes.system_message): | 
|  | return result | 
|  | (figure_node,) = result | 
|  | # wrap figure node into a kernel_figure node / see visitors | 
|  | node = kernel_figure('', figure_node) | 
|  | return [node] | 
|  |  | 
|  |  | 
|  | # render handling | 
|  | # --------------------- | 
|  |  | 
|  | def visit_kernel_render(self, node): | 
|  | """Visitor of the ``kernel_render`` Node. | 
|  |  | 
|  | If rendering tools available, save the markup of the ``literal_block`` child | 
|  | node into a file and replace the ``literal_block`` node with a new created | 
|  | ``image`` node, pointing to the saved markup file. Afterwards, handle the | 
|  | image child-node with the ``convert_image(...)``. | 
|  | """ | 
|  | app = self.builder.app | 
|  | srclang = node.get('srclang') | 
|  |  | 
|  | app.verbose('visit kernel-render node lang: "%s"' % (srclang)) | 
|  |  | 
|  | tmp_ext = RENDER_MARKUP_EXT.get(srclang, None) | 
|  | if tmp_ext is None: | 
|  | app.warn('kernel-render: "%s" unknown / include raw.' % (srclang)) | 
|  | return | 
|  |  | 
|  | if not dot_cmd and tmp_ext == '.dot': | 
|  | app.verbose("dot from graphviz not available / include raw.") | 
|  | return | 
|  |  | 
|  | literal_block = node[0] | 
|  |  | 
|  | code      = literal_block.astext() | 
|  | hashobj   = code.encode('utf-8') #  str(node.attributes) | 
|  | fname     = path.join('%s-%s' % (srclang, sha1(hashobj).hexdigest())) | 
|  |  | 
|  | tmp_fname = path.join( | 
|  | self.builder.outdir, self.builder.imagedir, fname + tmp_ext) | 
|  |  | 
|  | if not path.isfile(tmp_fname): | 
|  | mkdir(path.dirname(tmp_fname)) | 
|  | with open(tmp_fname, "w") as out: | 
|  | out.write(code) | 
|  |  | 
|  | img_node = nodes.image(node.rawsource, **node.attributes) | 
|  | img_node['uri'] = path.join(self.builder.imgpath, fname + tmp_ext) | 
|  | img_node['candidates'] = { | 
|  | '*': path.join(self.builder.imgpath, fname + tmp_ext)} | 
|  |  | 
|  | literal_block.replace_self(img_node) | 
|  | convert_image(img_node, self, tmp_fname) | 
|  |  | 
|  |  | 
|  | class kernel_render(nodes.General, nodes.Inline, nodes.Element): | 
|  | """Node for ``kernel-render`` directive.""" | 
|  | pass | 
|  |  | 
|  | class KernelRender(Figure): | 
|  | u"""KernelRender directive | 
|  |  | 
|  | Render content by external tool.  Has all the options known from the | 
|  | *figure*  directive, plus option ``caption``.  If ``caption`` has a | 
|  | value, a figure node with the *caption* is inserted. If not, a image node is | 
|  | inserted. | 
|  |  | 
|  | The KernelRender directive wraps the text of the directive into a | 
|  | literal_block node and wraps it into a kernel_render node. See | 
|  | ``visit_kernel_render``. | 
|  | """ | 
|  | has_content = True | 
|  | required_arguments = 1 | 
|  | optional_arguments = 0 | 
|  | final_argument_whitespace = False | 
|  |  | 
|  | # earn options from 'figure' | 
|  | option_spec = Figure.option_spec.copy() | 
|  | option_spec['caption'] = directives.unchanged | 
|  |  | 
|  | def run(self): | 
|  | return [self.build_node()] | 
|  |  | 
|  | def build_node(self): | 
|  |  | 
|  | srclang = self.arguments[0].strip() | 
|  | if srclang not in RENDER_MARKUP_EXT.keys(): | 
|  | return [self.state_machine.reporter.warning( | 
|  | 'Unknown source language "%s", use one of: %s.' % ( | 
|  | srclang, ",".join(RENDER_MARKUP_EXT.keys())), | 
|  | line=self.lineno)] | 
|  |  | 
|  | code = '\n'.join(self.content) | 
|  | if not code.strip(): | 
|  | return [self.state_machine.reporter.warning( | 
|  | 'Ignoring "%s" directive without content.' % ( | 
|  | self.name), | 
|  | line=self.lineno)] | 
|  |  | 
|  | node = kernel_render() | 
|  | node['alt'] = self.options.get('alt','') | 
|  | node['srclang'] = srclang | 
|  | literal_node = nodes.literal_block(code, code) | 
|  | node += literal_node | 
|  |  | 
|  | caption = self.options.get('caption') | 
|  | if caption: | 
|  | # parse caption's content | 
|  | parsed = nodes.Element() | 
|  | self.state.nested_parse( | 
|  | ViewList([caption], source=''), self.content_offset, parsed) | 
|  | caption_node = nodes.caption( | 
|  | parsed[0].rawsource, '', *parsed[0].children) | 
|  | caption_node.source = parsed[0].source | 
|  | caption_node.line = parsed[0].line | 
|  |  | 
|  | figure_node = nodes.figure('', node) | 
|  | for k,v in self.options.items(): | 
|  | figure_node[k] = v | 
|  | figure_node += caption_node | 
|  |  | 
|  | node = figure_node | 
|  |  | 
|  | return node | 
|  |  | 
|  | def add_kernel_figure_to_std_domain(app, doctree): | 
|  | """Add kernel-figure anchors to 'std' domain. | 
|  |  | 
|  | The ``StandardDomain.process_doc(..)`` method does not know how to resolve | 
|  | the caption (label) of ``kernel-figure`` directive (it only knows about | 
|  | standard nodes, e.g. table, figure etc.). Without any additional handling | 
|  | this will result in a 'undefined label' for kernel-figures. | 
|  |  | 
|  | This handle adds labels of kernel-figure to the 'std' domain labels. | 
|  | """ | 
|  |  | 
|  | std = app.env.domains["std"] | 
|  | docname = app.env.docname | 
|  | labels = std.data["labels"] | 
|  |  | 
|  | for name, explicit in iteritems(doctree.nametypes): | 
|  | if not explicit: | 
|  | continue | 
|  | labelid = doctree.nameids[name] | 
|  | if labelid is None: | 
|  | continue | 
|  | node = doctree.ids[labelid] | 
|  |  | 
|  | if node.tagname == 'kernel_figure': | 
|  | for n in node.next_node(): | 
|  | if n.tagname == 'caption': | 
|  | sectname = clean_astext(n) | 
|  | # add label to std domain | 
|  | labels[name] = docname, labelid, sectname | 
|  | break |