Source code for qutip_qip.circuit.circuit_latex

import collections
import functools
import os
import sys
import shutil
import subprocess
import tempfile
import warnings


def _run_command(command, *args, **kwargs):
    """
    Run a command with stdout explicitly thrown away, raising
    `RuntimeError` with the system error message
    if the command returned a non-zero exit code.
    """
    try:
        return subprocess.run(
            command,
            *args,
            check=True,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.PIPE,
            **kwargs,
        )
    except subprocess.CalledProcessError as e:
        raise RuntimeError(e.stderr.decode(sys.stderr.encoding)) from None


def _force_remove(*filenames):
    """`rm -f`: try to remove a file, ignoring errors if it doesn't exist."""
    for filename in filenames:
        try:
            os.remove(filename)
        except FileNotFoundError:
            pass


def _test_convert_is_imagemagick():
    """
    Test to see if the `convert` command behaves like we'd expect ImageMagick
    to.  On Windows if ImageMagick is not installed then `convert` may refer to
    a system utility.
    """
    try:
        # Don't use `capture_output` because we're still supporting Python 3.6
        process = subprocess.run(
            ("convert", "-version"),
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
        )
        return "imagemagick" in process.stdout.decode("utf-8").lower()
    except FileNotFoundError:
        return False


_SPECIAL_CASES = {
    "convert": _test_convert_is_imagemagick,
}


def _find_system_command(names):
    """
    Given a list of possible system commands (as strings), return the first one
    which has a locatable executable form, or `None` if none of them do.  We
    also check some special cases of shadowing (e.g. ImageMagick 6's `convert`
    is also a Windows system utility) to try and catch false-positives.
    """
    for name in names:
        if shutil.which(name) is not None:
            is_valid = _SPECIAL_CASES.get(name, lambda: True)()
            if is_valid:
                return name
    return None


_pdflatex = _find_system_command(["pdflatex"])
_pdfcrop = _find_system_command(["pdfcrop"])


if _pdfcrop is not None:

    def _crop_pdf(filename):
        """Crop the pdf file `filename` in place."""
        temporary = ".tmp." + filename
        _run_command((_pdfcrop, filename, temporary))
        # Windows does not allow renaming to an existing file (but unix does).
        _force_remove(filename)
        os.rename(temporary, filename)

else:

    def _crop_pdf(_):
        # Warn, but do not raise - we can recover from a failed crop.
        warnings.warn(
            "Could not locate system 'pdfcrop':"
            " image output may have additional margins."
        )


def _convert_pdf(file_stem):
    """
    'Convert' to pdf: since LaTeX outputs a PDF file, there's nothing to do.
    """
    with open(file_stem + ".pdf", "rb") as file:
        return file.read()


# Record type to hold definitions of possible conversions - this is just for
# reading convenience.
_ConverterConfiguration = collections.namedtuple(
    "_ConverterConfiguration",
    ["file_type", "dependency", "executables", "arguments", "binary"],
)
CONVERTERS = {"pdf": _convert_pdf}
_MISSING_CONVERTERS = {}
_CONVERTER_CONFIGURATIONS = [
    _ConverterConfiguration(
        "png",
        "ImageMagick",
        ["magick", "convert"],
        arguments=("-density", "100"),
        binary=True,
    ),
    _ConverterConfiguration(
        "svg", "pdf2svg", ["pdf2svg"], arguments=(), binary=False
    ),
]


def _make_converter(configuration):
    """
    Create the actual conversion function of signature
        file_stem: str -> 'T,
    where 'T is data in the format to be converted to.
    """
    which = _find_system_command(configuration.executables)
    if which is None:
        return None
    mode = "rb" if configuration.binary else "r"

    def converter(file_stem, dpi=100):
        """
        Convert a file located in the current directory named `<file_stem>.pdf`
        to an image format with the name `<file_stem>.xxx`, where `xxx` is
        converter-dependent.

        Parameters
        ----------
        file_stem : str
            The basename of the PDF file to be converted.
        dpi : int/float
            Image density in dots per inch. Ignored for SVG.
        """
        in_file = file_stem + ".pdf"
        out_file = file_stem + "." + configuration.file_type
        if "-density" in configuration.arguments:
            arguments = list(configuration.arguments)
            arguments[arguments.index("-density") + 1] = str(dpi)
        else:
            arguments = configuration.arguments
        _run_command((which, *arguments, in_file, out_file))
        with open(out_file, mode) as file:
            return file.read()

    return converter


for configuration in _CONVERTER_CONFIGURATIONS:
    # Make the converter using a higher-order function, because if we defined a
    # function in the loop, it would be easy to later introduce bugs due to
    # leaky closures over loop variables.
    converter = _make_converter(configuration)
    if converter:
        CONVERTERS[configuration.file_type] = converter
    else:
        _MISSING_CONVERTERS[configuration.file_type] = configuration.dependency


if _pdflatex is not None:

    def image_from_latex(code, file_type="png", dpi=100):
        """
        Convert the LaTeX `code` into an image format, defined by the
        `file_type`.  Returns a string or bytes object, depending on whether
        the requested type is textual (e.g. svg) or binary (e.g. png).  The
        known file types are in keys in this module's `CONVERTERS` dictionary.

        Parameters
        ----------
        code: str
            LaTeX code representing the circuit to be converted.

        file_type: str ("png")
            The file type that the image should be returned in.


        Returns
        -------
        image: str or bytes
            An encoded version of the image.  Whether the output type is str or
            bytes depends on whether the requested image format is textual or
            binary.
        """
        filename = "qcirc"  # Arbitrary and internal.
        # We do all the image conversion in a temporary directory to prevent
        # leftover files if something goes wrong (or we get a
        # KeyboardInterrupt) during conversion.
        previous_dir = os.getcwd()
        with tempfile.TemporaryDirectory() as temporary_dir:
            try:
                os.chdir(temporary_dir)
                with open(filename + ".tex", "w") as file:
                    file.write(code)
                try:
                    _run_command(
                        (_pdflatex, "-interaction", "batchmode", filename)
                    )
                except RuntimeError as e:
                    message = (
                        "pdflatex failed."
                        " Perhaps you do not have it installed, or you are"
                        " missing the LaTeX package 'qcircuit'."
                    )
                    message += (
                        "The latex code is printed below. "
                        "Please try to compile locally using pdflatex:\n"
                        + code
                    )
                    raise RuntimeError(message) from e
                _crop_pdf(filename + ".pdf")
                if file_type in _MISSING_CONVERTERS:
                    dependency = _MISSING_CONVERTERS[file_type]
                    message = "".join(
                        [
                            "Could not find system ",
                            dependency,
                            ".",
                            " Image conversion to '",
                            file_type,
                            "'",
                            " is not available.",
                        ]
                    )
                    raise RuntimeError(message)
                if file_type not in CONVERTERS:
                    raise ValueError(
                        "".join(["Unknown output format: '", file_type, "'."])
                    )
                out = CONVERTERS[file_type](filename, dpi)
            finally:
                # Leave the temporary directory before it is removed (necessary
                # on Windows, but it doesn't hurt on POSIX).
                os.chdir(previous_dir)
        return out

else:

[docs] def image_from_latex(*args, **kwargs): raise RuntimeError("Could not find system 'pdflatex'.")