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