Source code for innoconv.ext.tikz2svg

r"""
Convert and insert Ti\ *k*\Z figures.

SVG files are rendered from Ti\ *k*\Z code and saved in the folder ``_tikz`` in
the static folder of the output directory.

Ti\ *k*\Z code blocks are replaced by image elements.

.. note::

    In order to use this extension you need to have the following software
    installed on your system:

    * LaTeX distribution with PGF/Ti\ *k*\Z
    * `pdf2svg <https://github.com/dawbarton/pdf2svg>`_

-------
Example
-------
A Ti\ *k*\Z image is directly embedded into Markdown using a code block.

.. code-block:: latex

  ```tikz
  \\begin{tikzpicture}
  \\shade[left color=blue,right color=red,rounded corners=8pt] (-0.5,-0.5)
    rectangle (2.5,3.45);
  \\draw[white,thick,dashed,rounded corners=8pt] (0,0) -- (0,2) -- (1,3.25)
    -- (2,2) -- (2,0) -- (0,2) -- (2,2) -- (0,0) -- (2,0);
  \\node[white] at (1,-0.25) {\\footnotesize House of Santa Claus};
  \\end{tikzpicture}
  ```

During conversion the code block will be turned into an image tag, similar
to the following.

.. code-block:: md

  ![](/_tikz/tikz_abcdef0123456789.svg "Alt text")

"""


from hashlib import md5
from logging import critical, info
from os import mkdir
from os.path import join
from shutil import copytree, rmtree
from subprocess import PIPE, Popen
from tempfile import TemporaryDirectory

from innoconv.constants import ENCODING, STATIC_FOLDER
from innoconv.ext.abstract import AbstractExtension
from innoconv.traverse_ast import TraverseAst

TEX_FILE_TEMPLATE = r"""
\documentclass{{standalone}}
\usepackage{{tikz}}
{preamble}
\begin{{document}}
\tikzset{{every picture/.style={{
  scale=1.4,every node/.style={{scale=1.4}}}}
}}
{tikz_code}
\end{{document}}
"""
CMD_PDFLATEX = "pdflatex -halt-on-error -jobname {} -file-line-error --"
CMD_PDF2SVG = "pdf2svg {} {}"
TIKZ_FOLDER = "_tikz"
TIKZ_FILENAME = "tikz_{}"
TIKZ_IMG_TAG_ALT = "TikZ Image"


[docs]class Tikz2Svg(AbstractExtension): r"""Convert and insert Ti\ *k*\Z images.""" _helptext = "Convert TikZ code to SVG files." def __init__(self, *args, **kwargs): """Initialize variables.""" super(Tikz2Svg, self).__init__(*args, **kwargs) self._output_dir = None self._tikz_images = None @staticmethod def _run(cmd, cwd, stdin=None): pipe = Popen(cmd, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE, cwd=cwd) if stdin: pipe.stdin.write(stdin.encode(ENCODING)) pipe.stdin.close() pipe.wait() if pipe.returncode != 0: critical(cmd) critical("Error: {}".format(pipe.returncode)) critical("Printing program stdout:") critical(pipe.stdout.read().decode(ENCODING)) critical("Printing program stderr:") critical(pipe.stderr.read().decode(ENCODING)) raise RuntimeError("Tikz2Pdf: Error converting to PDF!") @staticmethod def _get_tikz_name(tikz_hash): return TIKZ_FILENAME.format(tikz_hash) def _tikz_found(self, element, caption=None): """Remember TikZ code and replace with image.""" code = element["c"][1].strip() tikz_hash = md5(code.encode()).hexdigest() self._tikz_images[tikz_hash] = code filename = "{}.svg".format(Tikz2Svg._get_tikz_name(tikz_hash)) element["t"] = "Image" element["c"] = [ ["", [], []], caption or [], [join(TIKZ_FOLDER, filename), TIKZ_IMG_TAG_ALT], ] info("Found TikZ image {}".format(filename)) def _parse_tikz(self, elem, parent): try: if ( parent and parent["t"] == "Div" and "figure" in parent["c"][0][1] and parent["c"][1][0]["t"] == "Para" ): caption = parent["c"][1].pop(0)["c"] self._tikz_found(elem, caption) return except KeyError: pass self._tikz_found(elem) def _render_and_copy_tikz(self): info("Compiling {} TikZ images.".format(len(self._tikz_images))) if not self._tikz_images: return with TemporaryDirectory(prefix="innoconv-tikz2pdf-") as tmp_dir: pdf_path = join(tmp_dir, "pdf_out") mkdir(pdf_path) svg_path = join(tmp_dir, "svg_out") mkdir(svg_path) for tikz_hash, tikz_code in self._tikz_images.items(): file_base = Tikz2Svg._get_tikz_name(tikz_hash) # generate tex document try: preamble = self._manifest.tikz_preamble except AttributeError: preamble = "" # as this template is used with .format() we need to escape # curly brackets preamble.replace("{", "{{") preamble.replace("}", "}}") texdoc = TEX_FILE_TEMPLATE.format( tikz_code=tikz_code, preamble=preamble ) info("Compiling {}".format(file_base)) # compile tex cmd = CMD_PDFLATEX.format(join("pdf_out", file_base)) self._run(cmd, tmp_dir, stdin=texdoc) # convert to SVG pdf_filename = join(pdf_path, "{}.pdf".format(file_base)) svg_filename = join(svg_path, "{}.svg".format(file_base)) cmd = CMD_PDF2SVG.format(pdf_filename, svg_filename) self._run(cmd, tmp_dir) # copy SVG files tikz_path = join(self._output_dir, STATIC_FOLDER, TIKZ_FOLDER) try: copytree(svg_path, tikz_path) except FileExistsError: rmtree(tikz_path) copytree(svg_path, tikz_path)
[docs] def process_element(self, elem, parent): """Respond to AST element.""" try: if elem["t"] == "CodeBlock" and "tikz" in elem["c"][0][1]: self._parse_tikz(elem, parent) except (TypeError, KeyError): pass
# extension events
[docs] def start(self, output_dir, source_dir): """Initialize the list of images to be converted.""" self._tikz_images = dict() self._output_dir = output_dir
[docs] def post_process_file(self, ast, _, __): """Find TikZ images in AST and replace with image tags.""" TraverseAst(self.process_element).traverse(ast)
[docs] def finish(self): """Render images and copy SVG files to the static folder.""" self._render_and_copy_tikz()