Added type hints

This commit is contained in:
Andrew Murray 2024-08-26 23:49:03 +10:00
parent 4721c31b19
commit 53bbfea763
6 changed files with 67 additions and 44 deletions

View File

@ -1,6 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Union
import pytest
@ -8,6 +9,20 @@ from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper
if TYPE_CHECKING:
import PyQt6
import PySide6
QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication]
QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel]
QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint]
QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion]
QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -20,7 +35,7 @@ if ImageQt.qt_is_installed:
from PySide6.QtGui import QImage, QPainter, QRegion
from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget
class Example(QWidget):
class Example(QWidget): # type: ignore[misc]
def __init__(self) -> None:
super().__init__()
@ -28,11 +43,12 @@ if ImageQt.qt_is_installed:
qimage = ImageQt.ImageQt(img)
pixmap1 = ImageQt.QPixmap.fromImage(qimage)
pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage)
QHBoxLayout(self) # hbox
# hbox
QHBoxLayout(self) # type: ignore[operator]
lbl = QLabel(self)
lbl = QLabel(self) # type: ignore[operator]
# Segfault in the problem
lbl.setPixmap(pixmap1.copy())
@ -46,7 +62,7 @@ def roundtrip(expected: Image.Image) -> None:
@pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed")
def test_sanity(tmp_path: Path) -> None:
# Segfault test
app: QApplication | None = QApplication([])
app: QApplication | None = QApplication([]) # type: ignore[operator]
ex = Example()
assert app # Silence warning
assert ex # Silence warning
@ -56,7 +72,7 @@ def test_sanity(tmp_path: Path) -> None:
im = hopper(mode)
data = ImageQt.toqpixmap(im)
assert isinstance(data, QPixmap)
assert data.__class__.__name__ == "QPixmap"
assert not data.isNull()
# Test saving the file
@ -64,14 +80,14 @@ def test_sanity(tmp_path: Path) -> None:
data.save(tempfile)
# Render the image
qimage = ImageQt.ImageQt(im)
data = QPixmap.fromImage(qimage)
qt_format = QImage.Format if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, qt_format.Format_ARGB32)
painter = QPainter(qimage)
image_label = QLabel()
imageqt = ImageQt.ImageQt(im)
data = getattr(QPixmap, "fromImage")(imageqt)
qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage
qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator]
painter = QPainter(qimage) # type: ignore[operator]
image_label = QLabel() # type: ignore[operator]
image_label.setPixmap(data)
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128))
image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator]
painter.end()
rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png")
qimage.save(rendered_tempfile)

View File

@ -21,7 +21,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode)
data = ImageQt.toqimage(src)
assert isinstance(data, QImage)
assert isinstance(data, QImage) # type: ignore[arg-type, misc]
assert not data.isNull()
# reload directly from the qimage

View File

@ -166,5 +166,4 @@ warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
'^Tests/test_qt_image_qapplication.py$',
]

View File

@ -71,7 +71,7 @@ def Ghostscript(
fp: IO[bytes],
scale: int = 1,
transparency: bool = False,
) -> Image.Image:
) -> Image.core.ImagingCore:
"""Render an image using Ghostscript"""
global gs_binary
if not has_ghostscript():

View File

@ -223,7 +223,7 @@ if TYPE_CHECKING:
from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
@ -2978,7 +2978,7 @@ class Image:
self.load()
return self._new(self.im.effect_spread(distance))
def toqimage(self):
def toqimage(self) -> ImageQt.ImageQt:
"""Returns a QImage copy of this image"""
from . import ImageQt
@ -2987,7 +2987,7 @@ class Image:
raise ImportError(msg)
return ImageQt.toqimage(self)
def toqpixmap(self):
def toqpixmap(self) -> ImageQt.QPixmap:
"""Returns a QPixmap copy of this image"""
from . import ImageQt
@ -3314,7 +3314,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
def fromqimage(im) -> ImageFile.ImageFile:
def fromqimage(im: ImageQt.QImage) -> ImageFile.ImageFile:
"""Creates an image instance from a QImage image"""
from . import ImageQt
@ -3324,7 +3324,7 @@ def fromqimage(im) -> ImageFile.ImageFile:
return ImageQt.fromqimage(im)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: ImageQt.QPixmap) -> ImageFile.ImageFile:
"""Creates an image instance from a QPixmap image"""
from . import ImageQt

View File

@ -19,14 +19,23 @@ from __future__ import annotations
import sys
from io import BytesIO
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Any, Callable, Union
from . import Image
from ._util import is_path
if TYPE_CHECKING:
import PyQt6
import PySide6
from . import ImageFile
QBuffer: type
QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray]
QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice]
QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage]
QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap]
qt_version: str | None
qt_versions = [
["6", "PyQt6"],
@ -37,10 +46,6 @@ qt_versions = [
qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for version, qt_module in qt_versions:
try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
@ -65,19 +70,20 @@ def rgb(r: int, g: int, b: int, a: int = 255) -> int:
return qRgba(r, g, b, a) & 0xFFFFFFFF
def fromqimage(im):
def fromqimage(im: QImage | QPixmap) -> ImageFile.ImageFile:
"""
:param im: QImage or PIL ImageQt object
"""
buffer = QBuffer()
qt_openmode: object
if qt_version == "6":
try:
qt_openmode = QIODevice.OpenModeFlag
qt_openmode = getattr(QIODevice, "OpenModeFlag")
except AttributeError:
qt_openmode = QIODevice.OpenMode
qt_openmode = getattr(QIODevice, "OpenMode")
else:
qt_openmode = QIODevice
buffer.open(qt_openmode.ReadWrite)
buffer.open(getattr(qt_openmode, "ReadWrite"))
# preserve alpha channel with png
# otherwise ppm is more friendly with Image.open
if im.hasAlphaChannel():
@ -93,7 +99,7 @@ def fromqimage(im):
return Image.open(b)
def fromqpixmap(im) -> ImageFile.ImageFile:
def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
return fromqimage(im)
@ -123,7 +129,7 @@ def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
return b"".join(new_data)
def _toqclass_helper(im):
def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
data = None
colortable = None
exclusive_fp = False
@ -135,30 +141,32 @@ def _toqclass_helper(im):
if is_path(im):
im = Image.open(im)
exclusive_fp = True
assert isinstance(im, Image.Image)
qt_format = QImage.Format if qt_version == "6" else QImage
qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
if im.mode == "1":
format = qt_format.Format_Mono
format = getattr(qt_format, "Format_Mono")
elif im.mode == "L":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
colortable = [rgb(i, i, i) for i in range(256)]
elif im.mode == "P":
format = qt_format.Format_Indexed8
format = getattr(qt_format, "Format_Indexed8")
palette = im.getpalette()
assert palette is not None
colortable = [rgb(*palette[i : i + 3]) for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
# Populate the 4th channel with 255
im = im.convert("RGBA")
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_RGB32
format = getattr(qt_format, "Format_RGB32")
elif im.mode == "RGBA":
data = im.tobytes("raw", "BGRA")
format = qt_format.Format_ARGB32
format = getattr(qt_format, "Format_ARGB32")
elif im.mode == "I;16":
im = im.point(lambda i: i * 256)
format = qt_format.Format_Grayscale16
format = getattr(qt_format, "Format_Grayscale16")
else:
if exclusive_fp:
im.close()
@ -174,8 +182,8 @@ def _toqclass_helper(im):
if qt_is_installed:
class ImageQt(QImage):
def __init__(self, im) -> None:
class ImageQt(QImage): # type: ignore[misc]
def __init__(self, im: Image.Image | str | QByteArray) -> None:
"""
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
class.
@ -199,10 +207,10 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"])
def toqimage(im) -> ImageQt:
def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
return ImageQt(im)
def toqpixmap(im):
def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
qimage = toqimage(im)
return QPixmap.fromImage(qimage)
return getattr(QPixmap, "fromImage")(qimage)