qt_exporter.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. """A qt exporter."""
  2. import os
  3. import sys
  4. import tempfile
  5. import time
  6. from traitlets import default
  7. from .html import HTMLExporter
  8. class QtExporter(HTMLExporter):
  9. """A qt exporter."""
  10. paginate = None
  11. format = ""
  12. @default("file_extension")
  13. def _file_extension_default(self):
  14. return ".html"
  15. def _check_launch_reqs(self):
  16. if sys.platform.startswith("win") and self.format == "png":
  17. msg = "Exporting to PNG using Qt is currently not supported on Windows."
  18. raise RuntimeError(msg)
  19. from .qt_screenshot import QT_INSTALLED # noqa: PLC0415
  20. if not QT_INSTALLED:
  21. msg = (
  22. f"PyQtWebEngine is not installed to support Qt {self.format.upper()} conversion. "
  23. f"Please install `nbconvert[qt{self.format}]` to enable."
  24. )
  25. raise RuntimeError(msg)
  26. from .qt_screenshot import QtScreenshot # noqa: PLC0415
  27. return QtScreenshot
  28. def _run_pyqtwebengine(self, html):
  29. ext = ".html"
  30. temp_file = tempfile.NamedTemporaryFile( # noqa: SIM115
  31. suffix=ext, delete=False
  32. )
  33. filename = f"{temp_file.name[: -len(ext)]}.{self.format}"
  34. with temp_file:
  35. temp_file.write(html.encode("utf-8"))
  36. try:
  37. QtScreenshot = self._check_launch_reqs()
  38. s = QtScreenshot()
  39. s.capture(f"file://{temp_file.name}", filename, self.paginate)
  40. finally:
  41. # Ensure the file is deleted even if pyqtwebengine raises an exception
  42. os.unlink(temp_file.name)
  43. # Prefer Qt's in-memory bytes, but fall back to reading the file on disk
  44. data = getattr(s, "data", b"")
  45. if (not data) and os.path.exists(filename):
  46. deadline = time.time() + 5.0
  47. while time.time() < deadline:
  48. try:
  49. if os.path.getsize(filename) > 0:
  50. break
  51. except OSError:
  52. pass
  53. time.sleep(0.05)
  54. if os.path.exists(filename) and os.path.getsize(filename) > 0:
  55. with open(filename, "rb") as f:
  56. data = f.read()
  57. # Best-effort cleanup of the generated output file
  58. try:
  59. if os.path.exists(filename):
  60. os.unlink(filename)
  61. except OSError:
  62. pass
  63. return data
  64. def from_notebook_node(self, nb, resources=None, **kw):
  65. """Convert from notebook node."""
  66. self._check_launch_reqs()
  67. html, resources = super().from_notebook_node(nb, resources=resources, **kw)
  68. self.log.info("Building %s", self.format.upper())
  69. data = self._run_pyqtwebengine(html)
  70. self.log.info("%s successfully created", self.format.upper())
  71. # convert output extension
  72. # the writer above required it to be html
  73. resources["output_extension"] = f".{self.format}"
  74. return data, resources