123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- #
- # The Python Imaging Library.
- # $Id$
- #
- # a simple Qt image interface.
- #
- # history:
- # 2006-06-03 fl: created
- # 2006-06-04 fl: inherit from QImage instead of wrapping it
- # 2006-06-05 fl: removed toimage helper; move string support to ImageQt
- # 2013-11-13 fl: add support for Qt5 (aurelien.ballier@cyclonit.com)
- #
- # Copyright (c) 2006 by Secret Labs AB
- # Copyright (c) 2006 by Fredrik Lundh
- #
- # See the README file for information on usage and redistribution.
- #
- from __future__ import annotations
- import sys
- from io import BytesIO
- 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"],
- ["side6", "PySide6"],
- ]
- # If a version has already been imported, attempt it first
- qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
- for version, qt_module in qt_versions:
- try:
- qRgba: Callable[[int, int, int, int], int]
- if qt_module == "PyQt6":
- from PyQt6.QtCore import QBuffer, QIODevice
- from PyQt6.QtGui import QImage, QPixmap, qRgba
- elif qt_module == "PySide6":
- from PySide6.QtCore import QBuffer, QIODevice
- from PySide6.QtGui import QImage, QPixmap, qRgba
- except (ImportError, RuntimeError):
- continue
- qt_is_installed = True
- qt_version = version
- break
- else:
- qt_is_installed = False
- qt_version = None
- def rgb(r: int, g: int, b: int, a: int = 255) -> int:
- """(Internal) Turns an RGB color into a Qt compatible color integer."""
- # use qRgb to pack the colors, and then turn the resulting long
- # into a negative integer with the same bitpattern.
- return qRgba(r, g, b, a) & 0xFFFFFFFF
- 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 = getattr(QIODevice, "OpenModeFlag")
- except AttributeError:
- qt_openmode = getattr(QIODevice, "OpenMode")
- else:
- qt_openmode = QIODevice
- buffer.open(getattr(qt_openmode, "ReadWrite"))
- # preserve alpha channel with png
- # otherwise ppm is more friendly with Image.open
- if im.hasAlphaChannel():
- im.save(buffer, "png")
- else:
- im.save(buffer, "ppm")
- b = BytesIO()
- b.write(buffer.data())
- buffer.close()
- b.seek(0)
- return Image.open(b)
- def fromqpixmap(im: QPixmap) -> ImageFile.ImageFile:
- return fromqimage(im)
- def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
- """
- converts each scanline of data from 8 bit to 32 bit aligned
- """
- bits_per_pixel = {"1": 1, "L": 8, "P": 8, "I;16": 16}[mode]
- # calculate bytes per line and the extra padding if needed
- bits_per_line = bits_per_pixel * width
- full_bytes_per_line, remaining_bits_per_line = divmod(bits_per_line, 8)
- bytes_per_line = full_bytes_per_line + (1 if remaining_bits_per_line else 0)
- extra_padding = -bytes_per_line % 4
- # already 32 bit aligned by luck
- if not extra_padding:
- return bytes
- new_data = [
- bytes[i * bytes_per_line : (i + 1) * bytes_per_line] + b"\x00" * extra_padding
- for i in range(len(bytes) // bytes_per_line)
- ]
- return b"".join(new_data)
- def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]:
- data = None
- colortable = None
- exclusive_fp = False
- # handle filename, if given instead of image name
- if hasattr(im, "toUtf8"):
- # FIXME - is this really the best way to do this?
- im = str(im.toUtf8(), "utf-8")
- if is_path(im):
- im = Image.open(im)
- exclusive_fp = True
- assert isinstance(im, Image.Image)
- qt_format = getattr(QImage, "Format") if qt_version == "6" else QImage
- if im.mode == "1":
- format = getattr(qt_format, "Format_Mono")
- elif im.mode == "L":
- format = getattr(qt_format, "Format_Indexed8")
- colortable = [rgb(i, i, i) for i in range(256)]
- elif im.mode == "P":
- 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 = getattr(qt_format, "Format_RGB32")
- elif im.mode == "RGBA":
- data = im.tobytes("raw", "BGRA")
- format = getattr(qt_format, "Format_ARGB32")
- elif im.mode == "I;16":
- im = im.point(lambda i: i * 256)
- format = getattr(qt_format, "Format_Grayscale16")
- else:
- if exclusive_fp:
- im.close()
- msg = f"unsupported image mode {repr(im.mode)}"
- raise ValueError(msg)
- size = im.size
- __data = data or align8to32(im.tobytes(), size[0], im.mode)
- if exclusive_fp:
- im.close()
- return {"data": __data, "size": size, "format": format, "colortable": colortable}
- if qt_is_installed:
- 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.
- :param im: A PIL Image object, or a file name (given either as
- Python string or a PyQt string object).
- """
- im_data = _toqclass_helper(im)
- # must keep a reference, or Qt will crash!
- # All QImage constructors that take data operate on an existing
- # buffer, so this buffer has to hang on for the life of the image.
- # Fixes https://github.com/python-pillow/Pillow/issues/1370
- self.__data = im_data["data"]
- super().__init__(
- self.__data,
- im_data["size"][0],
- im_data["size"][1],
- im_data["format"],
- )
- if im_data["colortable"]:
- self.setColorTable(im_data["colortable"])
- def toqimage(im: Image.Image | str | QByteArray) -> ImageQt:
- return ImageQt(im)
- def toqpixmap(im: Image.Image | str | QByteArray) -> QPixmap:
- qimage = toqimage(im)
- pixmap = getattr(QPixmap, "fromImage")(qimage)
- if qt_version == "6":
- pixmap.detach()
- return pixmap
|