Repository: Qiskit/qiskit

Issue:
Transpiler raises an exception "Command 'pdflatex -v' returned non-zero exit status 1" on Windows
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**: main branch (commit b419e3968f7a88ff84d7968107c17ee226871513)
- **Python version**: 3.7
- **Operating system**: Windows

### What is the current behavior?
A simple call to the transpiler causes an exception even if `pdflatex` is installed. Likely, this behavior was introduced in #6487.

```
C:\...\envs\dev-terra\python.exe C:/.../qiskit-terra/_sandbox/pdflatex_bug.py

Sorry, but pdflatex did not succeed.

The log file hopefully contains the information to get MiKTeX going again:

  C:\...\MiKTeX\2.9\miktex\log\pdflatex.log
pdflatex: major issue: So far, no MiKTeX administrator has checked for updates.
Traceback (most recent call last):
  File "C:/.../qiskit-terra/_sandbox/pdflatex_bug.py", line 1, in <module>
    from qiskit import QuantumCircuit, transpile
  File "C:\...\qiskit-terra\qiskit\__init__.py", line 52, in <module>
    from qiskit.execute_function import execute  # noqa
  File "C:\...\qiskit-terra\qiskit\execute_function.py", line 24, in <module>
    from qiskit.compiler import transpile, assemble, schedule
  File "C:\...\qiskit-terra\qiskit\compiler\__init__.py", line 34, in <module>
    from .transpiler import transpile
  File "C:\...\qiskit-terra\qiskit\compiler\transpiler.py", line 30, in <module>
    from qiskit.transpiler import Layout, CouplingMap, PropertySet, PassManager
  File "C:\...\qiskit-terra\qiskit\transpiler\__init__.py", line 418, in <module>
    from .passmanager import PassManager
  File "C:\...\qiskit-terra\qiskit\transpiler\passmanager.py", line 19, in <module>
    from qiskit.visualization import pass_manager_drawer
  File "C:\...\qiskit-terra\qiskit\visualization\__init__.py", line 129, in <module>
    from .circuit_visualization import circuit_drawer, HAS_PIL, HAS_PDFLATEX
  File "C:\...\qiskit-terra\qiskit\visualization\circuit_visualization.py", line 50, in <module>
    subprocess.run("pdflatex -v", check=True)
  File "C:\...\envs\dev-terra\lib\subprocess.py", line 512, in run
    output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command 'pdflatex -v' returned non-zero exit status 1.
```


### Steps to reproduce the problem
Run a script:
```python
from qiskit import QuantumCircuit, transpile

qc = QuantumCircuit(2)
qc.cx(0, 1)
tqc = transpile(qc)
```

### What is the expected behavior?
No exception must be risen.


### Suggested solutions
Additional checks should be added to `circuit_visualization.py`




Discussion:

Resolving patch:
diff --git a/qiskit/visualization/__init__.py b/qiskit/visualization/__init__.py
--- a/qiskit/visualization/__init__.py
+++ b/qiskit/visualization/__init__.py
@@ -126,7 +126,7 @@
 from qiskit.visualization.transition_visualization import visualize_transition
 from qiskit.visualization.array import array_to_latex
 
-from .circuit_visualization import circuit_drawer, HAS_PIL, HAS_PDFLATEX
+from .circuit_visualization import circuit_drawer, HAS_PIL, HAS_PDFLATEX, HAS_PDFTOCAIRO
 from .dag_visualization import dag_drawer
 from .exceptions import VisualizationError
 from .gate_map import plot_gate_map, plot_circuit_layout, plot_error_map
diff --git a/qiskit/visualization/circuit_visualization.py b/qiskit/visualization/circuit_visualization.py
--- a/qiskit/visualization/circuit_visualization.py
+++ b/qiskit/visualization/circuit_visualization.py
@@ -25,7 +25,6 @@
 any of the backends.
 """
 
-import errno
 import logging
 import os
 import subprocess
@@ -46,15 +45,56 @@
 from qiskit.visualization import utils
 from qiskit.visualization import matplotlib as _matplotlib
 
-try:
-    subprocess.run("pdflatex --version", check=True)
-    HAS_PDFLATEX = True
-except OSError as ex:
-    HAS_PDFLATEX = False
 
 logger = logging.getLogger(__name__)
 
 
+class _HasPdfLatexWrapper:
+    """Wrapper to lazily detect presence of the ``pdflatex`` command."""
+
+    def __init__(self):
+        self.has_pdflatex = None
+
+    def __bool__(self):
+        if self.has_pdflatex is None:
+            try:
+                subprocess.run(
+                    ["pdflatex", "-version"],
+                    check=True,
+                    stdout=subprocess.DEVNULL,
+                    stderr=subprocess.DEVNULL,
+                )
+                self.has_pdflatex = True
+            except (OSError, subprocess.SubprocessError):
+                self.has_pdflatex = False
+        return self.has_pdflatex
+
+
+class _HasPdfToCairoWrapper:
+    """Lazily detect the presence of the ``pdftocairo`` command."""
+
+    def __init__(self):
+        self.has_pdftocairo = None
+
+    def __bool__(self):
+        if self.has_pdftocairo is None:
+            try:
+                subprocess.run(
+                    ["pdftocairo", "-v"],
+                    check=True,
+                    stdout=subprocess.DEVNULL,
+                    stderr=subprocess.DEVNULL,
+                )
+                self.has_pdftocairo = True
+            except (OSError, subprocess.SubprocessError):
+                self.has_pdftocairo = False
+        return self.has_pdftocairo
+
+
+HAS_PDFLATEX = _HasPdfLatexWrapper()
+HAS_PDFTOCAIRO = _HasPdfToCairoWrapper()
+
+
 def circuit_drawer(
     circuit,
     scale=None,
@@ -395,11 +435,9 @@ def _latex_circuit_drawer(
         PIL.Image: an in-memory representation of the circuit diagram
 
     Raises:
-        OSError: usually indicates that ```pdflatex``` or ```pdftocairo``` is
-                 missing.
-        CalledProcessError: usually points to errors during diagram creation.
-        MissingOptionalLibraryError: if pillow is not installed
-        VisualizationError: If unsupported image format is given as filename extension.
+        MissingOptionalLibraryError: if pillow, pdflatex, or poppler are not installed
+        VisualizationError: if one of the conversion utilities failed for some internal or
+            file-access reason.
     """
     tmpfilename = "circuit"
     with tempfile.TemporaryDirectory() as tmpdirname:
@@ -417,8 +455,25 @@ def _latex_circuit_drawer(
             initial_state=initial_state,
             cregbundle=cregbundle,
         )
+        if not HAS_PDFLATEX:
+            raise MissingOptionalLibraryError(
+                libname="pdflatex",
+                name="LaTeX circuit drawing",
+                msg="You will likely need to install a full LaTeX distribution for your system",
+            )
+        if not HAS_PDFTOCAIRO:
+            raise MissingOptionalLibraryError(
+                libname="pdftocairo",
+                name="LaTeX circuit drawing",
+                msg="This is part of the 'poppler' set of PDF utilities",
+            )
+        if not HAS_PIL:
+            raise MissingOptionalLibraryError(
+                libname="pillow",
+                name="LaTeX circuit drawing",
+                pip_install="pip install pillow",
+            )
         try:
-
             subprocess.run(
                 [
                     "pdflatex",
@@ -430,55 +485,42 @@ def _latex_circuit_drawer(
                 stderr=subprocess.DEVNULL,
                 check=True,
             )
-        except OSError as ex:
-            if ex.errno == errno.ENOENT:
-                logger.warning(
-                    "WARNING: Unable to compile latex. "
-                    "Is `pdflatex` installed? "
-                    "Skipping latex circuit drawing..."
-                )
-            raise
-        except subprocess.CalledProcessError as ex:
+        except OSError as exc:
+            # OSError should generally not occur, because it's usually only triggered if `pdflatex`
+            # doesn't exist as a command, but we've already checked that.
+            raise VisualizationError("`pdflatex` command could not be run.") from exc
+        except subprocess.CalledProcessError as exc:
             with open("latex_error.log", "wb") as error_file:
-                error_file.write(ex.stdout)
+                error_file.write(exc.stdout)
             logger.warning(
-                "WARNING Unable to compile latex. "
-                "The output from the pdflatex command can "
-                "be found in latex_error.log"
+                "Unable to compile LaTeX. Perhaps you are missing the `qcircuit` package."
+                " The output from the `pdflatex` command is in `latex_error.log`."
             )
-            raise
-        else:
-            if not HAS_PIL:
-                raise MissingOptionalLibraryError(
-                    libname="pillow",
-                    name="latex drawer",
-                    pip_install="pip install pillow",
-                )
-            try:
-                base = os.path.join(tmpdirname, tmpfilename)
-                subprocess.run(
-                    ["pdftocairo", "-singlefile", "-png", "-q", base + ".pdf", base], check=True
-                )
-                image = Image.open(base + ".png")
-                image = utils._trim(image)
-                os.remove(base + ".png")
-                if filename:
-                    if filename.endswith(".pdf"):
-                        os.rename(base + ".pdf", filename)
-                    else:
-                        try:
-                            image.save(filename)
-                        except VisualizationError as ve:
-                            raise VisualizationError(
-                                "ERROR: filename parameter does not use a supported extension."
-                            ) from ve
-            except (OSError, subprocess.CalledProcessError) as ex:
-                logger.warning(
-                    "WARNING: Unable to convert pdf to image. "
-                    "Is `poppler` installed? "
-                    "Skipping circuit drawing..."
-                )
-                raise
+            raise VisualizationError(
+                "`pdflatex` call did not succeed: see `latex_error.log`."
+            ) from exc
+        base = os.path.join(tmpdirname, tmpfilename)
+        try:
+            subprocess.run(
+                ["pdftocairo", "-singlefile", "-png", "-q", base + ".pdf", base],
+                check=True,
+            )
+        except (OSError, subprocess.CalledProcessError) as exc:
+            message = "`pdftocairo` failed to produce an image."
+            logger.warning(message)
+            raise VisualizationError(message) from exc
+        image = Image.open(base + ".png")
+        image = utils._trim(image)
+        if filename:
+            if filename.endswith(".pdf"):
+                os.rename(base + ".pdf", filename)
+            else:
+                try:
+                    image.save(filename)
+                except (ValueError, OSError) as exc:
+                    raise VisualizationError(
+                        f"Pillow could not write the image file '{filename}'."
+                    ) from exc
         return image
 
 


Test patch:
diff --git a/test/python/visualization/test_circuit_drawer.py b/test/python/visualization/test_circuit_drawer.py
--- a/test/python/visualization/test_circuit_drawer.py
+++ b/test/python/visualization/test_circuit_drawer.py
@@ -27,6 +27,19 @@
     from matplotlib import figure
 
 
+_latex_drawer_condition = unittest.skipUnless(
+    all(
+        (
+            visualization.HAS_PYLATEX,
+            visualization.HAS_PIL,
+            visualization.HAS_PDFLATEX,
+            visualization.HAS_PDFTOCAIRO,
+        )
+    ),
+    "Skipped because not all of PIL, pylatex, pdflatex and pdftocairo are available",
+)
+
+
 class TestCircuitDrawer(QiskitTestCase):
     def test_default_output(self):
         with patch("qiskit.user_config.get_config", return_value={}):
@@ -77,25 +90,15 @@ def test_default_backend_auto_output_without_mpl(self):
                 out = visualization.circuit_drawer(circuit)
                 self.assertIsInstance(out, text.TextDrawing)
 
-    @unittest.skipUnless(
-        visualization.HAS_PYLATEX and visualization.HAS_PIL and visualization.HAS_PDFLATEX,
-        "Skipped because PIL and pylatex is not available",
-    )
-    def test_unsupported_image_format_error_message(self):
+    @_latex_drawer_condition
+    def test_latex_unsupported_image_format_error_message(self):
         with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "latex"}):
             circuit = QuantumCircuit()
-            with self.assertRaises(VisualizationError) as ve:
+            with self.assertRaises(VisualizationError, msg="Pillow could not write the image file"):
                 visualization.circuit_drawer(circuit, filename="file.spooky")
-                self.assertEqual(
-                    str(ve.exception),
-                    "ERROR: filename parameter does not use a supported extension.",
-                )
 
-    @unittest.skipUnless(
-        visualization.HAS_PYLATEX and visualization.HAS_PIL and visualization.HAS_PDFLATEX,
-        "Skipped because PIL and pylatex is not available",
-    )
-    def test_output_file_correct_format(self):
+    @_latex_drawer_condition
+    def test_latex_output_file_correct_format(self):
         with patch("qiskit.user_config.get_config", return_value={"circuit_drawer": "latex"}):
             circuit = QuantumCircuit()
             filename = "file.gif"