ImageQt.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #
  2. # The Python Imaging Library.
  3. # $Id$
  4. #
  5. # a simple Qt image interface.
  6. #
  7. # history:
  8. # 2006-06-03 fl: created
  9. # 2006-06-04 fl: inherit from QImage instead of wrapping it
  10. # 2006-06-05 fl: removed toimage helper; move string support to ImageQt
  11. # 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com)
  12. #
  13. # Copyright (c) 2006 by Secret Labs AB
  14. # Copyright (c) 2006 by Fredrik Lundh
  15. #
  16. # See the README file for information on usage and redistribution.
  17. #
  18. from __future__ import annotations
  19. import sys
  20. from io import BytesIO
  21. from typing import Any, Callable, Union
  22. from . import Image
  23. from ._util import is_path
  24. TYPE_CHECKING = False
  25. if TYPE_CHECKING:
  26. import PyQt6
  27. import PySide6
  28. from . import ImageFile
  29. QBuffer: type
  30. QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
  31. QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
  32. QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
  33. QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
  34. qt_version: str | None
  35. qt_versions = [
  36. ["6", "PyQt6"],
  37. ["side6", "PySide6"],
  38. ]
  39. # If a version has already been imported, attempt it first
  40. qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
  41. for version, qt_module in qt_versions:
  42. try:
  43. qRgba: Callable[[int, int, int, int], int]
  44. if qt_module == "PyQt6":
  45. from PyQt6.QtCore import QBuffer, QIODevice
  46. from PyQt6.QtGui import QImage, QPixmap, qRgba
  47. elif qt_module == "PySide6":
  48. from PySide6.QtCore import QBuffer, QIODevice
  49. from PySide6.QtGui import QImage, QPixmap, qRgba
  50. except (ImportError, RuntimeError):
  51. continue
  52. qt_is_installed = True
  53. qt_version = version
  54. break
  55. else:
  56. qt_is_installed = False
  57. qt_version = None
  58. def rgb(r: int, g: int, b: int, a: int = 255) -> int:
  59. """(Internal) Turns an RGB color into a Qt compatible color integer."""
  60. # use qRgb to pack the colors, and then turn the resulting long
  61. # into a negative integer with the same bitpattern.
  62. return qRgba(r, g, b, a) & 0xFFFFFFFF
  63. def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile:
  64. """
  65. :param im: QImage or PIL ImageQt object
  66. """
  67. buffer = QBuffer()
  68. qt_openmode: object
  69. if qt_version == "6":
  70. try:
  71. qt_openmode = getattr(QIODevice, "OpenModeFlag")
  72. except AttributeError:
  73. qt_openmode = getattr(QIODevice, "OpenMode")
  74. else:
  75. qt_openmode = QIODevice
  76. buffer.open(getattr(qt_openmode, "ReadWrite"))
  77. # preserve alpha channel with png
  78. # otherwise ppm is more friendly with Image.open
  79. if im.hasAlphaChannel():
  80. im.save(buffer, "png")
  81. else:
  82. im.save(buffer, "ppm")
  83. b = BytesIO()
  84. b.write(buffer.data())
  85. buffer.close()
  86. b.seek(0)
  87. return Image.open(b)
  88. def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
  89. return fromqimage(im)
  90. def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
  91. """
  92. converts each scanline of data from 8 bit to 32 bit aligned
  93. """
  94. bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode]
  95. # calculate bytes per line and the extra padding if needed
  96. bits_per_line = bits_per_pixel * width
  97. full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8)
  98. bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0)
  99. extra_padding = -bytes_per_line % 4
  100. # already 32 bit aligned by luck
  101. if not extra_padding:
  102. return bytes
  103. new_data = [
  104. bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
  105. for i in range(len(bytes) // bytes_per_line)
  106. ]
  107. return b"".join(new_data)
  108. def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
  109. data = None
  110. colortable = None
  111. exclusive_fp = False
  112. # handle filename, if given instead of image name
  113. if hasattr(im, "toUtf8"):
  114. # FIXME - is this really the best way to do this?
  115. im = str(im.toUtf8(), "utf-8")
  116. if is_path(im):
  117. im = Image.open(im)
  118. exclusive_fp = True
  119. assert isinstance(im, Image.Image)
  120. qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
  121. if im.mode == "1":
  122. format = getattr(qt_format, "Format_Mono")
  123. elif im.mode == "L":
  124. format = getattr(qt_format, "Format_Indexed8")
  125. colortable = [rgb(i, i, i) for i in range(256)]
  126. elif im.mode == "P":
  127. format = getattr(qt_format, "Format_Indexed8")
  128. palette = im.getpalette()
  129. assert palette is not None
  130. colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
  131. elif im.mode == "RGB":
  132. # Populate the 4th channel with 255
  133. im = im.convert("RGBA")
  134. data = im.tobytes("raw", "BGRA")
  135. format = getattr(qt_format, "Format_RGB32")
  136. elif im.mode == "RGBA":
  137. data = im.tobytes("raw", "BGRA")
  138. format = getattr(qt_format, "Format_ARGB32")
  139. elif im.mode == "I;16":
  140. im = im.point(lambda i: i * 256)
  141. format = getattr(qt_format, "Format_Grayscale16")
  142. else:
  143. if exclusive_fp:
  144. im.close()
  145. msg = f"unsupported image mode {repr(im.mode)}"
  146. raise ValueError(msg)
  147. size = im.size
  148. __data = data or align8to32(im.tobytes(), size[0], im.mode)
  149. if exclusive_fp:
  150. im.close()
  151. return {"data": __data, "size": size, "format": format, "colortable": colortable}
  152. if qt_is_installed:
  153. class ImageQt(QImage): # type: ignore[misc]
  154. def __init__(self, im: Image.Image | str | QByteArray) -> None:
  155. """
  156. An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
  157. class.
  158. :param im: A PIL Image object, or a file name (given either as
  159. Python string or a PyQt string object).
  160. """
  161. im_data = _toqclass_helper(im)
  162. # must keep a reference, or Qt will crash!
  163. # All QImage constructors that take data operate on an existing
  164. # buffer, so this buffer has to hang on for the life of the image.
  165. # Fixes https://github.com/python-pillow/Pillow/issues/1370
  166. self.__data = im_data["data"]
  167. super().__init__(
  168. self.__data,
  169. im_data["size"][0],
  170. im_data["size"][1],
  171. im_data["format"],
  172. )
  173. if im_data["colortable"]:
  174. self.setColorTable(im_data["colortable"])
  175. def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
  176. return ImageQt(im)
  177. def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
  178. qimage = toqimage(im)
  179. pixmap = getattr(QPixmap, "fromImage")(qimage)
  180. if qt_version == "6":
  181. pixmap.detach()
  182. return pixmap