Source code for svgt

#!/usr/bin/env python
# -*- coding: utf-8 -*-

# "THE WISKEY-WARE LICENSE":
# <jbc.develop@gmail.com> wrote this file. As long as you retain this notice you
# can do whatever you want with this stuff. If we meet some day, and you think
# this stuff is worth it, you can buy me a WISKEY in return Juan BC

#===============================================================================
# DOCS
#===============================================================================

"""
SVG Template (Directive)
========================

* **Directive Type:** "image"
* **Doctree Elements:** figure, image, caption, legend
* **Directive Arguments:** One, required (image URI).
* **Directive Options:** Possible.
* **Directive Content:** Interpreted as the figure caption and an optional legend.

A "svgt" is a figure
(http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure) rendered
from a svg replaced all ``${variables}`` with a given values.

You can create the svg with any software (like inkscape) and put inside
text variables formated like ``${variables}`` and it will replaced from the
argument ``vars``

**NOTE:** The directive ``depends`` on inkscape (http://inkscape.org/)

::

    .. svgt:: /path/to/svg_file.svg

        :vars: {"var_name0": "var_value0,
                "var_name1": "var_value1",
                 ...,
                "var_nameN": "var_valueN"}
        :dpi: 32
        :inkscape_dir: /path/to/inkscape/directory/

        This is the caption of the rendered svg (a simple paragraph).

        The legend consists of all elements **after** the caption.  In this
        case, the legend consists of this paragraph and the following
        table:

           +-----------------------+-----------------------+
           | Symbol                | Meaning               |
           +=======================+=======================+
           | .. image:: tent.png   | Campground            |
           +-----------------------+-----------------------+
           | .. image:: waves.png  | Lake                  |
           +-----------------------+-----------------------+
           | .. image:: peak.png   | Mountain              |
           +-----------------------+-----------------------+

There must be blank lines before the caption paragraph and before the
legend.  To specify a legend without a caption, use an empty comment
("..") in place of the caption.

The "svgt" directive supports all of the options of the "figure"
directive are passed on to the contained created figure.


In addition, the following options are recognized:

    ``vars``: json string
        An json string containing all the variable values to be replaced in 
        the svg.

    ``dpi``: non-negative int
        The dpi of the image generated from svg (default: 72).

    ``inkscape_dir``: path
        The directory where the binary of inkscape lives (default: "").

    ``list_vars_and_exit``: flag
        If you add this argument will be printed on standard output all the 
        variables that are in the svg.


"""


#===============================================================================
# META
#===============================================================================

__version__ = '0.2c'
__license__ = "THE WISKEY-WARE LICENSE"
__author__ = 'JuanBC'
__email__ = 'jbc.develop@gmail.com'
__url__ = 'https://bitbucket.org/leliel12/ninja-ide_plugins'
__date__ = '2012/02/15'


#===============================================================================
# IMPORTS
#===============================================================================

import subprocess
import shlex
import string
import tempfile
import os
import json
import sys
import shutil

from docutils import nodes
from docutils.parsers.rst import directives
from docutils.parsers.rst import Directive
from docutils.statemachine import StringList


#===============================================================================
# CONSTANTS
#===============================================================================

WARN = "svgt depends on inkscape (http://inkscape.org/)"

# {isp} the full path to the inkscape executable [default: ""]
# {svg} the svg to be rendered
# -z turn off the gui
# -e {png} the name of the render
# -d {dpi} the dpi of the png [default: 92]
CMD = "{isp} {svg} -z -e {png} -d {dpi}"


SPECS = {
    "vars": json.loads,
    "dpi": directives.nonnegative_int,
    "inkscape_dir": directives.path,
    "list_vars_and_exit": directives.flag,

    "alt": directives.unchanged,
    "height": directives.length_or_percentage_or_unitless,
    "width": directives.length_or_percentage_or_unitless,
    "scale": directives.percentage,
    "align": lambda align: directives.choice(align, ('left', 'center', 'right')),
    "target": directives.uri,
    "class": directives.unchanged,
    "name": directives.unchanged,
    "figwidth": directives.length_or_percentage_or_unitless,
    "figclass": directives.unchanged,
}


#===============================================================================
# CONF
#===============================================================================

_tempdir = None


#===============================================================================
# DIRECTIVE
#===============================================================================

[docs]class SVGTemplate(Directive): """ The svgt directive""" required_arguments = 0 optional_arguments = len(SPECS) final_argument_whitespace = False option_spec = SPECS has_content = True def _call(self, cmd): """Execute the cmd an return the standar output or raises an exception """ pcmd = shlex.split(cmd) p = subprocess.Popen(pcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() return stdout def _resolve_render_name(self, uri): """Resolve a unique name to be rendered in the _tempdir """ tempdir = _tempdir if _tempdir != None else tempfile.gettempdir() basename = os.path.basename(uri).rsplit(".", 1)[0] render_name = os.path.join(tempdir, basename) # svg name idx = "" unique_svg = render_name + "{idx}" + ".svg" while os.path.exists(unique_svg.format(idx=idx)): idx = idx + 1 if isinstance(idx, int) else 1 unique_svg = unique_svg.format(idx=idx) # png name idx = "" unique_png = render_name + "{idx}" + ".png" while os.path.exists(unique_png.format(idx=idx)): idx = idx + 1 if isinstance(idx, int) else 1 unique_png = unique_png.format(idx=idx) return unique_svg, unique_png def _render_svg(self, uri, svgt_isd, svgt_dpi, svgt_vars): """Render *svg* in to *png* and ``returns`` the png path. Arguments: ``uri`` The uri of the svg to be rendered. ``svgt_isd`` The directory of inkscape executable. ``svgt_dpi`` The *dpi* of the png to be rendered. ``svgt_vars`` A dic with all variables to be replaced in the svg """ with open(uri) as fp: svg_tplt = string.Template(fp.read()) svg_src = svg_tplt.safe_substitute(svgt_vars) fname_svg, fname_png = self._resolve_render_name(uri) with open(fname_svg, "w") as fp: fp.write(svg_src) # "{isp} {svg} -z -e {png} -d {dpi}" cmd = CMD.format(isp=os.path.join(svgt_isd, "inkscape"), svg=fname_svg, png=fname_png, dpi=svgt_dpi) self._call(cmd) return fname_png def _show_vars(self, uri): """Show variables inside the svg template """ with open(uri) as fp: svg_tplt = string.Template(fp.read()) variables = set() for e, n, b, i in svg_tplt.pattern.findall(svg_tplt.template): if n or b: variables.add(n or b) title = "Variables in '{uri}':".format(uri=uri) sub = "-" * len(title) variables = "\n".join(variables) msg = "{title}\n{sub}\n{vars}\n".format(title=title, sub=sub, uri=uri, vars=variables) sys.stdout.write(msg) def _separate_content(self, content): """Separate the conent of the figure in 2 parts one for the caption and other for the legend """ caption_cnt = StringList(parent=content.parent, parent_offset=content.parent_offset) legend_cnt = content # all nodes need to be procesed if content: caption_cnt.append(content[0], content.source(0), content.offset(0)) content.pop(0) return caption_cnt, legend_cnt def run(self): # retrieve svg uri uri = self.arguments[0] # extract svgt variables from options options = dict(self.options) svgt_vars = options.pop("vars") if "vars" in options else {} svgt_dpi = options.pop("dpi") if "dpi" in options else 72 svgt_isd = options.pop("inkscape_dir") if "inkscape_dir" in options else "" svgt_lvae = options.pop("list_vars_and_exit") == None \ if "list_vars_and_exit" in options else False # if svgt_lvae flag is active show all variables and exit if svgt_lvae: self._show_vars(uri) sys.exit(0) # prevent to pass align option to image fig_align = options.pop("align") if "align" in options else None # create the png to be embeded in the pdf png_path = self._render_svg(uri, svgt_isd, svgt_dpi, svgt_vars) # add uri of the png to image node options["uri"] = png_path image_node = nodes.image(self.block_text, **options) # create the label and legend for the figure caption_content, legend_content = self._separate_content(self.content) caption_node = nodes.caption("\n".join(caption_content)) self.state.nested_parse(caption_content, self.content_offset, caption_node) legend_node = nodes.legend("\n".join(legend_content)) self.state.nested_parse(legend_content, self.content_offset, legend_node) # restore algign for the figure if exists if fig_align != None: options["align"] = fig_align # create the figure figure_node = nodes.figure(self.block_text, image_node, caption_node, legend_node, **options) return [figure_node] #=============================================================================== # REGISTER #=============================================================================== # register the svgt in the valid directives
directives.register_directive("svgt", SVGTemplate) #=============================================================================== # SETUP FOR SPHINX #===============================================================================
[docs]def setup(app): """**SVG Template Sphinx Integration** This directive add a config value named ``tempdir`` (defaul: _temp) when the images are rendered. """ global _tempdir def reset(app, ex): if _tempdir != None and os.path.isdir(_tempdir): shutil.rmtree(_tempdir) if ex: raise ex app.add_config_value("tempdir", "_temp", "") _tempdir = app.config["tempdir"] reset(app, None) os.mkdir(_tempdir) app.connect("build-finished", reset) #=============================================================================== # MAIN #===============================================================================
if __name__ == "__main__": print(__doc__)