"""Image loading helpers with Pillow fallback for formats Qt cannot handle."""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import QBuffer, Qt
from PySide6.QtGui import QImage, QImageReader, QPixmap
from pbpicat.image_ops import is_jpeg, repair_jpeg_sos
def _pillow_to_qimage(pil_img) -> QImage:
if pil_img.mode != "RGBA":
pil_img = pil_img.convert("RGBA")
w, h = pil_img.size
data = pil_img.tobytes("raw", "RGBA")
img = QImage(data, w, h, QImage.Format.Format_RGBA8888)
return img.copy()
# Qt's default (256 MiB) is sized against the *undecoded* image dimensions, before
# any setScaledSize() downscaling is applied. Large but legitimate photos (high-MP
# phone cameras, panoramas) can exceed it, causing QImageReader to reject the image
# outright ("Rejecting image as it exceeds the current allocation limit") and
# forcing a much slower, full-resolution Pillow fallback just to produce a small
# thumbnail. Raise it generously while still guarding against corrupt files
# advertising absurd dimensions.
_ALLOCATION_LIMIT_MIB = 1024
def _make_reader(path: Path) -> tuple[QImageReader, QBuffer | None]:
"""Build a QImageReader for path, repairing known-malformed JPEG headers first.
Some camera firmware writes an invalid SOS marker that Qt merely warns
about ("Invalid SOS parameters for sequential JPEG"); repairing it up
front avoids the warning and the decode issues it can cause.
"""
if is_jpeg(path):
try:
data = repair_jpeg_sos(path.read_bytes())
except OSError:
reader = QImageReader(str(path))
reader.setAllocationLimit(_ALLOCATION_LIMIT_MIB)
return reader, None
buf = QBuffer()
buf.setData(data)
buf.open(QBuffer.OpenModeFlag.ReadOnly)
reader = QImageReader(buf, b"jpg")
reader.setAllocationLimit(_ALLOCATION_LIMIT_MIB)
return reader, buf
reader = QImageReader(str(path))
reader.setAllocationLimit(_ALLOCATION_LIMIT_MIB)
return reader, None
[docs]
def load_qimage(path: Path, max_w: int = 0, max_h: int = 0, auto_rotate: bool = True) -> QImage:
"""Load an image as QImage, with optional scaling. Thread-safe."""
reader, _buf = _make_reader(path)
reader.setAutoTransform(auto_rotate)
if max_w > 0 and max_h > 0:
orig = reader.size()
if orig.isValid():
reader.setScaledSize(orig.scaled(max_w, max_h, Qt.KeepAspectRatio))
image = reader.read()
if not image.isNull():
return image
try:
from PIL import Image, ImageOps
pil_img = Image.open(str(path))
pil_img.load()
if auto_rotate:
pil_img = ImageOps.exif_transpose(pil_img)
if max_w > 0 and max_h > 0:
pil_img.thumbnail((max_w, max_h), Image.LANCZOS)
return _pillow_to_qimage(pil_img)
except Exception: # noqa: BLE001
return QImage()
[docs]
def load_pixmap(path: Path, auto_rotate: bool = True) -> QPixmap:
"""Load an image as QPixmap. Must be called from the main thread."""
reader, _buf = _make_reader(path)
reader.setAutoTransform(auto_rotate)
image = reader.read()
if not image.isNull():
return QPixmap.fromImage(image)
try:
from PIL import Image, ImageOps
pil_img = Image.open(str(path))
pil_img.load()
if auto_rotate:
pil_img = ImageOps.exif_transpose(pil_img)
img = _pillow_to_qimage(pil_img)
if not img.isNull():
return QPixmap.fromImage(img)
except Exception: # noqa: BLE001
pass
return QPixmap()