1
0
Fork 0
mirror of synced 2024-05-18 19:42:54 +12:00
Rare/rare/widgets/image_widget.py
2024-02-24 02:22:25 +02:00

159 lines
5.7 KiB
Python

from enum import Enum
from typing import Tuple, Optional, Union
from PyQt5.QtCore import Qt, QRectF, QSize
from PyQt5.QtGui import (
QPaintEvent,
QPainter,
QPixmap,
QTransform,
QBrush,
QPalette,
QPainterPath,
QLinearGradient,
QColor,
)
from PyQt5.QtWidgets import QWidget
from rare.models.image import ImageSize
OverlayPath = Tuple[QPainterPath, Union[QColor, QLinearGradient]]
class ImageWidget(QWidget):
class Border(Enum):
Rounded = 0
Squared = 1
_pixmap: Optional[QPixmap] = None
_opacity: float = 1.0
_transform: QTransform
_smooth_transform: bool = False
_rounded_overlay: Optional[OverlayPath] = None
_squared_overlay: Optional[OverlayPath] = None
_image_size: Optional[ImageSize.Preset] = None
def __init__(self, parent=None) -> None:
super(ImageWidget, self).__init__(parent=parent)
self.setObjectName(type(self).__name__)
self.setContentsMargins(0, 0, 0, 0)
self.paint_image = self.paint_image_empty
self.paint_overlay = self.paint_overlay_rounded
def setOpacity(self, value: float) -> None:
self._opacity = value
self.update()
def setPixmap(self, pixmap: QPixmap) -> None:
if not pixmap.isNull():
self._pixmap = pixmap
self.paint_image = self.paint_image_cover
if not self._image_size:
self._transform = QTransform().scale(
1 / pixmap.devicePixelRatioF(),
1 / pixmap.devicePixelRatioF(),
)
else:
self._transform = QTransform().scale(
1 / pixmap.devicePixelRatioF() / self._image_size.divisor,
1 / pixmap.devicePixelRatioF() / self._image_size.divisor,
)
else:
self.paint_image = self.paint_image_empty
self.update()
def sizeHint(self) -> QSize:
return self._image_size.size if self._image_size else super(ImageWidget, self).sizeHint()
def minimumSizeHint(self) -> QSize:
return self._image_size.size if self._image_size else super(ImageWidget, self).minimumSizeHint()
def setFixedSize(self, a0: ImageSize.Preset) -> None:
self._squared_overlay = None
self._rounded_overlay = None
self._image_size = a0
self._smooth_transform = a0.smooth
super(ImageWidget, self).setFixedSize(a0.size)
def setBorder(self, border: Border):
if border == ImageWidget.Border.Rounded:
self.paint_overlay = self.paint_overlay_rounded
else:
self.paint_overlay = self.paint_overlay_squared
self.update()
def _generate_squared_overlay(self) -> OverlayPath:
if self._image_size is not None and self._squared_overlay is not None:
return self._squared_overlay
path = QPainterPath()
path.addRect(0, 0, self.width(), self.height())
border = 2
inner_path = QPainterPath()
inner_path.addRect(
QRectF(
border,
border,
self.width() - border * 2,
self.height() - border * 2,
)
)
gradient = QLinearGradient(0, 0, 0, self.height())
gradient.setColorAt(0.0, Qt.black)
gradient.setColorAt(1.0, Qt.transparent)
self._squared_overlay = path.subtracted(inner_path), gradient
return self._squared_overlay
def _generate_rounded_overlay(self) -> OverlayPath:
if self._image_size is not None and self._rounded_overlay is not None:
return self._rounded_overlay
# lk: the '-1' and '+1' are adjustments required for anti-aliasing
# lk: otherwise vertical lines would appear at the edges
path = QPainterPath()
path.addRect(-1, -1, self.width() + 2, self.height() + 2)
rounded_path = QPainterPath()
rounded_path.addRoundedRect(
QRectF(0, 0, self.width(), self.height()),
self.height() * 0.045,
self.height() * 0.045,
)
self._rounded_overlay = path.subtracted(rounded_path), QColor(0, 0, 0, 0)
return self._rounded_overlay
def paint_image_empty(self, painter: QPainter, a0: QPaintEvent) -> None:
# when pixmap object is not available yet, show a gray rectangle
painter.setOpacity(0.5 * self._opacity)
painter.fillRect(a0.rect(), Qt.darkGray)
def paint_image_cover(self, painter: QPainter, a0: QPaintEvent) -> None:
painter.setOpacity(self._opacity)
brush = QBrush(self._pixmap)
# downscale the image during painting to fit the pixelratio
brush.setTransform(self._transform)
painter.fillRect(a0.rect(), brush)
def paint_overlay_rounded(self, painter: QPainter, a0: QPaintEvent) -> None:
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setOpacity(1.0)
painter.setCompositionMode(QPainter.CompositionMode_Source)
overlay, _ = self._generate_rounded_overlay()
painter.fillPath(overlay, self.palette().color(QPalette.Window))
def paint_overlay_squared(self, painter: QPainter, a0: QPaintEvent) -> None:
painter.setRenderHint(QPainter.Antialiasing, False)
painter.setOpacity(self._opacity)
painter.fillPath(*self._generate_squared_overlay())
def paintEvent(self, a0: QPaintEvent) -> None:
painter = QPainter(self)
if not painter.paintEngine().isActive():
return
# helps with better image quality
painter.setRenderHint(QPainter.SmoothPixmapTransform, self._smooth_transform)
self.paint_image(painter, a0)
self.paint_overlay(painter, a0)
painter.end()
__all__ = ["ImageSize", "ImageWidget"]