refactor: split Qt functions from thumb renderer

This commit is contained in:
Travis Abendshien
2026-07-25 18:55:19 -07:00
parent cd6e427e02
commit e2c9d30ed3
6 changed files with 189 additions and 143 deletions
+110 -135
View File
@@ -7,31 +7,23 @@ import hashlib
import math
from copy import deepcopy
from pathlib import Path
from typing import TYPE_CHECKING
import structlog
from PIL import (
Image,
ImageChops,
ImageDraw,
ImageEnhance,
ImageFile,
ImageQt,
UnidentifiedImageError,
)
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFile, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PySide6.QtCore import QObject, QSize, Qt, Signal
from PySide6.QtGui import QGuiApplication, QPixmap
from typing_extensions import deprecated
from tagstudio.core.exceptions import NoRendererError
from tagstudio.core.library.alchemy.library import Library
from tagstudio.core.library.ignore import Ignore
from tagstudio.core.media_types import MediaCategories, MediaType
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.global_settings import (
DEFAULT_CACHED_THUMB_RES,
MAX_CACHED_THUMB_RES,
MIN_CACHED_THUMB_RES,
GlobalSettings,
Theme,
)
from tagstudio.qt.helpers.gradients import four_corner_gradient
from tagstudio.qt.models.palette import UI_COLORS, ColorType, UiColor, get_ui_color
@@ -45,7 +37,7 @@ from tagstudio.renderers.archive import (
)
from tagstudio.renderers.audio import audio_album_thumb, audio_waveform_thumb
from tagstudio.renderers.blender import blender_thumb
from tagstudio.renderers.clip_studio import clip_studio_thumb, pdn_thumb
from tagstudio.renderers.clip_studio import clip_studio_thumb, paint_dot_net_thumb
from tagstudio.renderers.ebook import epub_thumb
from tagstudio.renderers.font import font_full_preview, font_small_thumb
from tagstudio.renderers.medibang_paint import medibang_paint_thumb
@@ -56,9 +48,6 @@ from tagstudio.renderers.text import text_thumb
from tagstudio.renderers.vector_image import vector_image_thumb
from tagstudio.renderers.video import video_thumb
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
ImageFile.LOAD_TRUNCATED_IMAGES = True
Image.MAX_IMAGE_PIXELS = None
@@ -66,18 +55,16 @@ Image.MAX_IMAGE_PIXELS = None
logger = structlog.get_logger(__name__)
class ThumbRenderer(QObject):
class FileRenderer:
"""A class for rendering image and file thumbnails."""
rm: ResourceManager = ResourceManager()
updated = Signal(float, QPixmap, QSize, Path)
updated_ratio = Signal(float)
cached_img_ext: str = ".webp"
def __init__(self, driver: "QtDriver") -> None:
"""Initialize the class."""
def __init__(self, library: Library, settings: GlobalSettings) -> None:
super().__init__()
self.driver = driver
self.lib = library
self.settings = settings
# Cached thumbnail elements.
# Key: Size + Pixel Ratio Tuple + Radius Scale
@@ -120,7 +107,6 @@ class ThumbRenderer(QObject):
return "file_generic"
@deprecated("This method will be replaced with Qt painting methods in the near future.")
def _get_mask(
self, size: tuple[int, int], pixel_ratio: float, scale_radius: bool = False
) -> Image.Image:
@@ -144,7 +130,6 @@ class ThumbRenderer(QObject):
self.thumb_masks[(*size, pixel_ratio, radius_scale)] = item
return item
@deprecated("This method will be replaced with Qt painting methods in the near future.")
def _get_edge(
self, size: tuple[int, int], pixel_ratio: float
) -> tuple[Image.Image, Image.Image]:
@@ -167,6 +152,7 @@ class ThumbRenderer(QObject):
name: str,
color: UiColor,
size: tuple[int, int],
theme: Theme,
pixel_ratio: float = 1.0,
bg_image: Image.Image | None = None,
draw_edge: bool = True,
@@ -178,6 +164,7 @@ class ThumbRenderer(QObject):
name (str): The name of the icon resource. "thumb_loading" will not draw a border.
color (str): The color to use for the icon.
size (tuple[int,int]): The size of the icon.
theme (Theme): A theme enum to determine the light/dark theme.
pixel_ratio (float): The screen pixel ratio.
bg_image (Image.Image): Optional background image to go behind the icon.
draw_edge (bool): Flag for is the raised edge should be drawn.
@@ -190,19 +177,20 @@ class ThumbRenderer(QObject):
item: Image.Image | None = self.icons.get((name, color, *size, pixel_ratio))
if not item:
item_flat: Image.Image = (
self._render_corner_icon(name, color, size, pixel_ratio, bg_image)
self._render_corner_icon(name, color, size, pixel_ratio, theme, bg_image)
if is_corner
else self._render_center_icon(name, color, size, pixel_ratio, draw_border, bg_image)
else self._render_center_icon(
name, color, size, pixel_ratio, theme, draw_border, bg_image
)
)
if draw_edge:
edge: tuple[Image.Image, Image.Image] = self._get_edge(size, pixel_ratio)
item = self._apply_edge(item_flat, edge, faded=True)
item = self._apply_edge(item_flat, edge, theme, faded=True)
self.icons[(name, color, *size, pixel_ratio)] = item
else:
item = item_flat
return item
@deprecated("This method will be replaced with Qt painting methods in the near future.")
def _render_mask(
self, size: tuple[int, int], pixel_ratio: float, radius_scale: float = 1
) -> Image.Image:
@@ -233,7 +221,6 @@ class ThumbRenderer(QObject):
)
return im
@deprecated("This method will be replaced with Qt painting methods in the near future.")
def _render_edge(
self, size: tuple[int, int], pixel_ratio: float
) -> tuple[Image.Image, Image.Image]:
@@ -293,6 +280,7 @@ class ThumbRenderer(QObject):
color: UiColor,
size: tuple[int, int],
pixel_ratio: float,
theme: Theme,
draw_border: bool = True,
bg_image: Image.Image | None = None,
) -> Image.Image:
@@ -303,6 +291,7 @@ class ThumbRenderer(QObject):
color (UiColor): The color to use for the icon.
size (tuple[int,int]): The size of the icon.
pixel_ratio (float): The screen pixel ratio.
theme (Theme): A theme enum to determine the light/dark theme.
draw_border (bool): Option to draw a border.
bg_image (Image.Image): Optional background image to go behind the icon.
"""
@@ -362,11 +351,7 @@ class ThumbRenderer(QObject):
size,
resample=Image.Resampling.BILINEAR,
)
fg: Image.Image = Image.new(
"RGB",
size=size,
color="#00FF00",
)
fg: Image.Image = Image.new("RGB", size=size, color="#00FF00")
# Get icon by name
icon = self.rm.get(name)
@@ -388,10 +373,7 @@ class ThumbRenderer(QObject):
)
# Apply color overlay
im = self._apply_overlay_color(
im,
color,
)
im = self._apply_overlay_color(im, color, theme)
return im
@@ -401,6 +383,7 @@ class ThumbRenderer(QObject):
color: UiColor,
size: tuple[int, int],
pixel_ratio: float,
theme: Theme,
bg_image: Image.Image | None = None,
) -> Image.Image:
"""Render a thumbnail icon with the icon in the upper-left corner.
@@ -410,6 +393,7 @@ class ThumbRenderer(QObject):
color (UiColor): The color to use for the icon.
size (tuple[int,int]): The size of the icon.
pixel_ratio (float): The screen pixel ratio.
theme (Theme): A theme enum to determine the light/dark theme.
draw_border (bool): Option to draw a border.
bg_image (Image.Image): Optional background image to go behind the icon.
"""
@@ -436,10 +420,7 @@ class ThumbRenderer(QObject):
color="#000000",
)
# Apply color overlay
bg = self._apply_overlay_color(
im,
color,
)
bg = self._apply_overlay_color(im, color, theme)
# Paste background color with rounded rectangle mask onto blank image
im.paste(
@@ -493,7 +474,7 @@ class ThumbRenderer(QObject):
return im
def _apply_overlay_color(self, image: Image.Image, color: UiColor) -> Image.Image:
def _apply_overlay_color(self, image: Image.Image, color: UiColor, theme: Theme) -> Image.Image:
"""Apply a color overlay effect to an image based on its color channel data.
Red channel for foreground, green channel for outline, none for background.
@@ -501,20 +482,21 @@ class ThumbRenderer(QObject):
Args:
image (Image.Image): The image to apply an overlay to.
color (UiColor): The name of the ColorType color to use.
theme (Theme): A theme enum to determine the light/dark theme.
"""
bg_color: str = (
get_ui_color(ColorType.DARK_ACCENT, color)
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
if theme == Theme.DARK
else get_ui_color(ColorType.PRIMARY, color)
)
fg_color: str = (
get_ui_color(ColorType.PRIMARY, color)
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
if theme == Theme.DARK
else get_ui_color(ColorType.LIGHT_ACCENT, color)
)
ol_color: str = (
get_ui_color(ColorType.BORDER, color)
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
if theme == Theme.DARK
else get_ui_color(ColorType.LIGHT_ACCENT, color)
)
@@ -534,9 +516,12 @@ class ThumbRenderer(QObject):
return bg
@deprecated("This method will be replaced with Qt painting methods in the near future.")
def _apply_edge(
self, image: Image.Image, edge: tuple[Image.Image, Image.Image], faded: bool = False
self,
image: Image.Image,
edge: tuple[Image.Image, Image.Image],
theme: Theme,
faded: bool = False,
) -> Image.Image:
"""Apply a given edge effect to an image.
@@ -544,13 +529,12 @@ class ThumbRenderer(QObject):
image (Image.Image): The image to apply the edge to.
edge (tuple[Image.Image, Image.Image]): The edge images to apply.
Item 0 is the inner highlight, and item 1 is the outer shadow.
theme (Theme): A theme enum to determine the light/dark theme.
faded (bool): Whether to apply a faded version of the edge.
Used for light themes.
"""
opacity: float = 1.0 if not faded else 0.8
shade_reduction: float = (
0 if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark else 0.3
)
shade_reduction: float = 0 if theme == Theme.DARK else 0.3
im: Image.Image = image
im_hl, im_sh = deepcopy(edge)
@@ -570,21 +554,24 @@ class ThumbRenderer(QObject):
def render(
self,
cache: CacheManager | None,
timestamp: float,
filepath: Path | str,
base_size: tuple[int, int],
pixel_ratio: float,
theme: Theme = Theme.DARK,
is_loading: bool = False,
is_grid_thumb: bool = False,
update_on_ratio_change: bool = False,
):
"""Render a thumbnail or preview image.
Args:
cache (CacheManager | None): A cache manager instance.
timestamp (float): The timestamp for which this job was dispatched.
filepath (str | Path): The path of the file to render a thumbnail for.
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
pixel_ratio (float): The screen pixel ratio.
theme (Theme): A theme enum to determine the light/dark theme.
is_loading (bool): Is this a loading graphic?
is_grid_thumb (bool): Is this a thumbnail for the thumbnail grid?
Or else the Preview Pane?
@@ -592,11 +579,7 @@ class ThumbRenderer(QObject):
"""
render_mask_and_edge: bool = True
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixel_ratio)
theme_color: UiColor = (
UiColor.THEME_LIGHT
if QGuiApplication.styleHints().colorScheme() == Qt.ColorScheme.Light
else UiColor.THEME_DARK
)
theme_color: UiColor = UiColor.THEME_LIGHT if theme == Theme.LIGHT else UiColor.THEME_DARK
if isinstance(filepath, str):
filepath = Path(filepath)
@@ -605,6 +588,7 @@ class ThumbRenderer(QObject):
name=self._get_resource_id(filepath),
color=theme_color,
size=size,
theme=theme,
pixel_ratio=pixel_ratio,
)
return im
@@ -616,6 +600,7 @@ class ThumbRenderer(QObject):
name="broken_link_icon",
color=UiColor.RED,
size=size,
theme=theme,
pixel_ratio=pixel_ratio,
bg_image=cached_im,
draw_edge=not cached_im,
@@ -623,9 +608,7 @@ class ThumbRenderer(QObject):
)
return im
def render_ignored(
size: tuple[int, int], pixel_ratio: float, im: Image.Image
) -> Image.Image:
def render_ignored(size: tuple[int, int], im: Image.Image) -> Image.Image:
icon_ratio: float = 5
padding_factor = 18
@@ -646,8 +629,9 @@ class ThumbRenderer(QObject):
def fetch_cached_image(file_name: Path):
image: Image.Image | None = None
assert self.driver.cache_manager is not None
cached_path = self.driver.cache_manager.get_file_path(file_name)
if not cache:
return image
cached_path = cache.get_file_path(file_name)
if cached_path and cached_path.is_file():
try:
@@ -669,11 +653,11 @@ class ThumbRenderer(QObject):
mod_time = str(filepath.stat().st_mtime_ns)
hashable_str: str = f"{str(filepath)}{mod_time}"
hash_value = hashlib.shake_128(hashable_str.encode("utf-8")).hexdigest(8)
file_name = Path(f"{hash_value}{ThumbRenderer.cached_img_ext}")
file_name = Path(f"{hash_value}{FileRenderer.cached_img_ext}")
image = fetch_cached_image(file_name)
if not image and self.driver.settings.generate_thumbs:
settings_res = self.driver.settings.cached_thumb_resolution
if not image and self.settings.generate_thumbs:
settings_res = self.settings.cached_thumb_resolution
thumb_res = (
settings_res
if settings_res >= MIN_CACHED_THUMB_RES and settings_res <= MAX_CACHED_THUMB_RES
@@ -684,10 +668,11 @@ class ThumbRenderer(QObject):
# TODO: Audio waveforms are dynamically sized based on the base_size, so hardcoding
# the resolution breaks that.
image = self._render(
timestamp,
cache,
filepath,
(thumb_res, thumb_res),
1,
theme,
is_grid_thumb,
save_to_file=file_name,
)
@@ -711,7 +696,7 @@ class ThumbRenderer(QObject):
(adj_size, adj_size), pixel_ratio
)
image = self._apply_edge(
four_corner_gradient(image, (adj_size, adj_size), mask), edge
four_corner_gradient(image, (adj_size, adj_size), mask), edge, theme
)
# Check if the file is supposed to be ignored and render an overlay if needed
@@ -720,10 +705,10 @@ class ThumbRenderer(QObject):
image
and Ignore.compiled_patterns
and Ignore.compiled_patterns.match(
filepath.relative_to(unwrap(self.driver.lib.library_dir))
filepath.relative_to(unwrap(self.lib.library_dir))
)
):
image = render_ignored((adj_size, adj_size), pixel_ratio, image)
image = render_ignored((adj_size, adj_size), image)
except TypeError:
pass
@@ -731,13 +716,13 @@ class ThumbRenderer(QObject):
elif is_loading:
# Initialize "Loading" thumbnail
loading_thumb: Image.Image = self._get_icon(
"thumb_loading", theme_color, (adj_size, adj_size), pixel_ratio
"thumb_loading", theme_color, (adj_size, adj_size), theme, pixel_ratio
)
image = loading_thumb.resize((adj_size, adj_size), resample=Image.Resampling.BILINEAR)
# A full preview image (never cached)
elif not is_grid_thumb:
image = self._render(timestamp, filepath, base_size, pixel_ratio)
image = self._render(cache, filepath, base_size, pixel_ratio, theme)
if not image:
image = (
render_unlinked((512, 512), 2)
@@ -754,40 +739,31 @@ class ThumbRenderer(QObject):
if not image:
image = Image.new("RGBA", (128, 128), color="#FF00FF")
# Convert the final image to a pixmap to emit.
qim = ImageQt.ImageQt(image)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixel_ratio)
self.updated_ratio.emit(image.size[0] / image.size[1])
if pixmap:
self.updated.emit(
timestamp,
pixmap,
QSize(
math.ceil(adj_size / pixel_ratio),
math.ceil(image.size[1] / pixel_ratio),
),
filepath,
)
else:
self.updated.emit(timestamp, QPixmap(), QSize(*base_size), filepath)
return (
image,
(math.ceil(adj_size / pixel_ratio), math.ceil(image.size[1] / pixel_ratio)),
timestamp,
)
def _render(
self,
timestamp: float,
cache: CacheManager | None,
filepath: str | Path,
base_size: tuple[int, int],
pixel_ratio: float,
theme: Theme = Theme.DARK,
is_grid_thumb: bool = False,
save_to_file: Path | None = None,
) -> Image.Image | None:
"""Render a thumbnail or preview image.
Args:
cache (CacheManager | None): A cache manager instance.
timestamp (float): The timestamp for which this job was dispatched.
filepath (str | Path): The path of the file to render a thumbnail for.
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
pixel_ratio (float): The screen pixel ratio.
theme (Theme): A theme enum to determine the light/dark theme.
is_grid_thumb (bool): Is this a thumbnail for the thumbnail grid?
Or else the Preview Pane?
save_to_file(Path | None): A filepath to optionally save the output to.
@@ -795,117 +771,117 @@ class ThumbRenderer(QObject):
"""
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixel_ratio)
image: Image.Image | None = None
_filepath: Path = Path(filepath)
filepath_: Path = Path(filepath)
savable_media_type: bool = True
if _filepath and _filepath.is_file():
if filepath_ and filepath_.is_file():
try:
ext: str = _filepath.suffix.lower() if _filepath.suffix else _filepath.stem.lower()
# Ebooks =======================================================
ext: str = filepath_.suffix.lower() if filepath_.suffix else filepath_.stem.lower()
# eBooks ===========================================================================
if MediaCategories.is_ext_in_category(
ext, MediaCategories.EBOOK_TYPES, mime_fallback=True
):
image = epub_thumb(_filepath, ext)
# Krita ========================================================
image = epub_thumb(filepath_, ext)
# Krita ============================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
):
image = krita_thumb(_filepath)
# Clip Studio Paint ============================================
image = krita_thumb(filepath_)
# Clip Studio Paint ================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.CLIP_STUDIO_PAINT_TYPES
):
image = clip_studio_thumb(_filepath)
# VTF ==========================================================
image = clip_studio_thumb(filepath_)
# VTF ==============================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True
):
image = vtf_thumb(_filepath)
# Images =======================================================
image = vtf_thumb(filepath_)
# Images ===========================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_TYPES, mime_fallback=True
):
# Raw Images -----------------------------------------------
# Raw Images -------------------------------------------------------------------
if MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
):
image = raw_image_thumb(_filepath)
# Vector Images --------------------------------------------
image = raw_image_thumb(filepath_)
# Vector Images ----------------------------------------------------------------
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
):
image = vector_image_thumb(_filepath, adj_size)
# EXR Images -----------------------------------------------
image = vector_image_thumb(filepath_, adj_size)
# EXR Images -------------------------------------------------------------------
elif ext in [".exr"]:
image = exr_image_thumb(_filepath)
# Normal Images --------------------------------------------
image = exr_image_thumb(filepath_)
# Normal Images ----------------------------------------------------------------
else:
image = raster_image_thumb(_filepath)
# Videos =======================================================
image = raster_image_thumb(filepath_)
# Videos ===========================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
):
image = video_thumb(_filepath)
# PowerPoint Slideshow
image = video_thumb(filepath_)
# PowerPoint =======================================================================
elif ext in {".pptx"}:
image = powerpoint_thumb(_filepath)
# OpenDocument/OpenOffice ======================================
image = powerpoint_thumb(filepath_)
# OpenDocument/OpenOffice ==========================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True
):
image = open_doc_thumb(_filepath)
# Apple iWork Suite ============================================
image = open_doc_thumb(filepath_)
# Apple iWork + Creator Studio =====================================================
elif (
MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES)
or ext == ".pxd"
):
image = apple_embedded_thumb(_filepath)
# Plain Text ===================================================
image = apple_embedded_thumb(filepath_)
# Plain Text =======================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True
):
image = text_thumb(_filepath)
# Fonts ========================================================
image = text_thumb(filepath_)
# Fonts ============================================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.FONT_TYPES, mime_fallback=True
):
if is_grid_thumb:
# Short (Aa) Preview
image = font_small_thumb(_filepath, adj_size)
image = font_small_thumb(filepath_, adj_size)
if image is not None:
image = self._apply_overlay_color(image, UiColor.BLUE)
image = self._apply_overlay_color(image, UiColor.BLUE, theme)
else:
# Large (Full Alphabet) Preview
image = font_full_preview(_filepath, adj_size)
image = font_full_preview(filepath_, adj_size)
# Audio ========================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.AUDIO_TYPES, mime_fallback=True
):
image = audio_album_thumb(_filepath, ext)
image = audio_album_thumb(filepath_, ext)
if image is None:
image = audio_waveform_thumb(_filepath, ext, adj_size, pixel_ratio)
image = audio_waveform_thumb(filepath_, ext, adj_size, pixel_ratio)
savable_media_type = False
if image is not None:
image = self._apply_overlay_color(image, UiColor.GREEN)
image = self._apply_overlay_color(image, UiColor.GREEN, theme)
# Blender ======================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.BLENDER_TYPES, mime_fallback=True
):
image = blender_thumb(_filepath)
image = blender_thumb(filepath_)
# PDF ==========================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.PDF_TYPES, mime_fallback=True
):
image = pdf_thumb(_filepath, adj_size, ext)
image = pdf_thumb(filepath_, adj_size, ext)
# Archives =====================================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.ARCHIVE_TYPES):
image = archive_thumb(_filepath, ext)
image = archive_thumb(filepath_, ext)
# MDIPACK ======================================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.MDIPACK_TYPES):
image = medibang_paint_thumb(_filepath)
image = medibang_paint_thumb(filepath_)
# Paint.NET ====================================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.PAINT_DOT_NET_TYPES):
image = pdn_thumb(_filepath)
image = paint_dot_net_thumb(filepath_)
# No Rendered Thumbnail ========================================
if not image:
raise NoRendererError
@@ -913,9 +889,8 @@ class ThumbRenderer(QObject):
if image:
image = self._resize_image(image, (adj_size, adj_size))
if save_to_file and savable_media_type and image:
assert self.driver.cache_manager is not None
self.driver.cache_manager.save_image(image, save_to_file, mode="RGBA")
if save_to_file and savable_media_type and image and cache:
cache.save_image(image, save_to_file, mode="RGBA")
except (
AssertionError,
+60
View File
@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from pathlib import Path
from PIL import ImageQt
from PySide6.QtCore import QObject, QSize, Signal
from PySide6.QtGui import QGuiApplication, QPixmap, Qt
from tagstudio.core.library.alchemy.library import Library
from tagstudio.qt.cache_manager import CacheManager
from tagstudio.qt.global_settings import GlobalSettings, Theme
from tagstudio.qt.previews.renderer import FileRenderer
class QtFileRenderer(QObject):
updated = Signal(float, QPixmap, QSize, Path)
updated_ratio = Signal(float)
"""A Qt-specific entry point for rendering file previews and thumbnails."""
def __init__(self, library: Library, settings: GlobalSettings) -> None:
super().__init__()
self.renderer = FileRenderer(library, settings)
self.theme = (
Theme.DARK
if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
else Theme.LIGHT
)
def render(
self,
cache: CacheManager | None,
timestamp: float,
filepath: Path | str,
base_size: tuple[int, int],
pixel_ratio: float,
is_loading: bool = False,
is_grid_thumb: bool = False,
):
image, size, timestamp = self.renderer.render(
cache=cache,
timestamp=timestamp,
filepath=filepath,
base_size=base_size,
pixel_ratio=pixel_ratio,
theme=self.theme,
is_loading=is_loading,
is_grid_thumb=is_grid_thumb,
)
qim = ImageQt.ImageQt(image)
pixmap = QPixmap.fromImage(qim)
pixmap.setDevicePixelRatio(pixel_ratio)
self.updated_ratio.emit(image.size[0] / image.size[1])
if pixmap:
self.updated.emit(timestamp, pixmap, QSize(size[0], size[1]), filepath)
else:
self.updated.emit(timestamp, QPixmap(), QSize(*base_size), filepath)
+12 -3
View File
@@ -17,7 +17,7 @@ from tagstudio.core.library.alchemy.enums import ItemType
from tagstudio.core.library.alchemy.models import Entry
from tagstudio.core.utils.types import unwrap
from tagstudio.qt.mixed.item_thumb import BadgeType, ItemThumb
from tagstudio.qt.previews.renderer import ThumbRenderer
from tagstudio.qt.qt_file_renderer import QtFileRenderer
if TYPE_CHECKING:
from tagstudio.qt.ts_qt import QtDriver
@@ -44,7 +44,7 @@ class ThumbGridLayout(QLayout):
self._entry_items: dict[int, int] = {}
self._render_results: dict[Path, Any] = {}
self._renderer: ThumbRenderer = ThumbRenderer(self.driver)
self._renderer: QtFileRenderer = QtFileRenderer(self.driver.lib, self.driver.settings)
self._renderer.updated.connect(self._on_rendered)
self._render_cutoff: float = 0.0
@@ -77,6 +77,7 @@ class ThumbGridLayout(QLayout):
(
self._renderer.render,
(
self.driver.cache_manager,
self._render_cutoff,
Path(),
base_size,
@@ -300,7 +301,15 @@ class ThumbGridLayout(QLayout):
self.driver.thumb_job_queue.put(
(
self._renderer.render,
(timestamp, file_path, base_size, ratio, False, True),
(
self.driver.cache_manager,
timestamp,
file_path,
base_size,
ratio,
False,
True,
),
)
)
+1 -1
View File
@@ -185,7 +185,7 @@ class QtDriver(DriverMixin, QObject):
applied_theme: Theme
lib: Library
cache_manager: CacheManager | None
cache_manager: CacheManager | None = None
browsing_history: History[BrowsingState]
+5 -3
View File
@@ -17,7 +17,7 @@ from tagstudio.core.media_types import MediaType
from tagstudio.qt.mixed.file_attributes import FileAttributeData
from tagstudio.qt.mixed.media_player import MediaPlayer
from tagstudio.qt.platform_strings import open_file_str, trash_term
from tagstudio.qt.previews.renderer import ThumbRenderer
from tagstudio.qt.qt_file_renderer import QtFileRenderer
from tagstudio.qt.translations import Translations
from tagstudio.qt.views.stylesheets.rounded_pixmap_style import RoundedPixmapStyle
@@ -45,6 +45,7 @@ class PreviewThumbView(QWidget):
def __init__(self, library: Library, driver: "QtDriver") -> None:
super().__init__()
self._driver = driver
self.__img_button_size = (266, 266)
self.__image_ratio = 1.0
@@ -109,7 +110,7 @@ class PreviewThumbView(QWidget):
self.__media_player_page = QWidget()
self.__stacked_page_setup(self.__media_player_page, self.__media_player)
self.__thumb_renderer = ThumbRenderer(driver)
self.__thumb_renderer = QtFileRenderer(driver.lib, driver.settings)
self.__thumb_renderer.updated.connect(self.__thumb_renderer_updated_callback)
self.__thumb_renderer.updated_ratio.connect(self.__thumb_renderer_updated_ratio_callback)
@@ -232,12 +233,13 @@ class PreviewThumbView(QWidget):
math.ceil(self.__img_button_size[1] * THUMB_SIZE_FACTOR),
)
# TODO: Make driver update the cache manager reference here instead of passing the driver.
self.__thumb_renderer.render(
self._driver.cache_manager,
time.time(),
filepath,
self.__rendered_res,
self.devicePixelRatio(),
update_on_ratio_change=True,
)
def __update_media_player(self, filepath: Path) -> None:
+1 -1
View File
@@ -44,7 +44,7 @@ def clip_studio_thumb(filepath: Path) -> Image.Image | None:
return im
def pdn_thumb(filepath: Path) -> Image.Image | None:
def paint_dot_net_thumb(filepath: Path) -> Image.Image | None:
"""Extract the base64-encoded thumbnail from a .pdn file header.
Args: