feat: dynamically load preview renderers

This commit is contained in:
Travis Abendshien
2026-08-22 15:05:11 -07:00
parent c6a2600165
commit d3a0432028
10 changed files with 54 additions and 49 deletions
+1
View File
@@ -86,6 +86,7 @@ class MediaTypes(metaclass=SanitizedAttr):
@staticmethod
def register(group: MediaTypeGroup) -> None:
# TODO: Allow for updating existing attributes instead of always creating a new one.
setattr(MediaTypes, group.name_key.replace(".", "_"), group)
+7 -2
View File
@@ -12,10 +12,15 @@ from tagstudio.core.enums import Theme
class BasePreview:
"""A base preview renderer class."""
"""A base preview renderer class.
Attributes:
media_type_name (str): Used for identifying the MediaType.
priority (int): Render priority over other Preview classes.
"""
# The attribute name used for identifying the MediaType used with the preview renderer.
media_type_name: str
priority: int = 50
def __init__(self) -> None:
pass
+39 -47
View File
@@ -1,10 +1,13 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
# SPDX-License-Identifier: MIT
import contextlib
import hashlib
import importlib
import inspect
import math
import pkgutil
from copy import deepcopy
from pathlib import Path
@@ -25,27 +28,6 @@ from tagstudio.core.utils.types import unwrap
from tagstudio.previews.base_preview import BasePreview
from tagstudio.previews.effects import apply_overlay_color
from tagstudio.previews.gradients import four_corner_gradient
from tagstudio.previews.renderers.apple_embedded_preview import AppleEmbeddedPreview
from tagstudio.previews.renderers.archive import ArchivePreview
from tagstudio.previews.renderers.audio import AudioPreview
from tagstudio.previews.renderers.blender import BlenderPreview
from tagstudio.previews.renderers.clip_studio import ClipStudioPaintPreview
from tagstudio.previews.renderers.code import CodePreview
from tagstudio.previews.renderers.ebook import EbookPreview
from tagstudio.previews.renderers.font import FontPreview
from tagstudio.previews.renderers.krita_preview import KritaPreview
from tagstudio.previews.renderers.medibang_paint import MediBangPaintPreview
from tagstudio.previews.renderers.open_document_preview import OpenDocumentPreview
from tagstudio.previews.renderers.paint_dot_net import PaintDotNetPreview
from tagstudio.previews.renderers.pdf import PdfPreview
from tagstudio.previews.renderers.powerpoint_preview import PowerPointPreview
from tagstudio.previews.renderers.raster_image import RasterImagePreview
from tagstudio.previews.renderers.raw_image_preview import RawImagePreview
from tagstudio.previews.renderers.text import (
TextPreview,
)
from tagstudio.previews.renderers.vector_image import VectorImagePreview
from tagstudio.previews.renderers.video import VideoPreview
from tagstudio.qt.app_settings import (
DEFAULT_CACHED_THUMB_RES,
MAX_CACHED_THUMB_RES,
@@ -63,11 +45,45 @@ Image.MAX_IMAGE_PIXELS = None
logger = structlog.get_logger(__name__)
# TODO: Allow user-created preview renderers from an external directory.
def _get_preview_renderers() -> list[type[BasePreview]]:
"""Discover all BasePreview subclasses in src/tagstudio/previews/renderers.
Classes are sorted by their priority (descending), falling back to alphabetical order.
"""
found: list[type[BasePreview]] = []
from tagstudio.previews import renderers # pyright: ignore
for module_info in sorted(pkgutil.iter_modules(renderers.__path__), key=lambda m: m.name):
module = importlib.import_module(f"{renderers.__name__}.{module_info.name}")
for _, obj in inspect.getmembers(module, inspect.isclass):
if (
issubclass(obj, BasePreview)
and obj is not BasePreview
and obj.__module__ == module.__name__
):
found.append(obj)
break
else:
logger.error("[FileRenderer] Could not load Preview Renderer", name=obj.__name__)
found.sort(key=lambda cls: cls.priority, reverse=True)
return found
class FileRenderer:
"""A class for rendering image previews and thumbnails from files."""
rm: ResourceManager = ResourceManager()
cached_img_ext: str = ".webp"
preview_renderers: list[type[BasePreview]] = _get_preview_renderers()
for pr in preview_renderers:
logger.info(
"[FileRenderer] Loaded Preview Renderer",
name=pr.__name__,
media_type=pr.media_type_name,
priority=pr.priority,
)
def __init__(self, library: Library, settings: AppSettings) -> None:
super().__init__()
@@ -717,34 +733,10 @@ class FileRenderer:
image: Image.Image | None = None
is_savable_type: bool = True
# TODO: Dynamically import these from the renderers/ directory at runtime,
# And allow user-created ones from an external directory.
previews: list[type[BasePreview]] = [
RawImagePreview,
VectorImagePreview,
RasterImagePreview,
VideoPreview,
AudioPreview,
CodePreview,
TextPreview,
PdfPreview,
ArchivePreview,
FontPreview,
BlenderPreview,
EbookPreview,
KritaPreview,
OpenDocumentPreview,
ClipStudioPaintPreview,
MediBangPaintPreview,
PaintDotNetPreview,
PowerPointPreview,
AppleEmbeddedPreview,
]
if filepath and filepath.is_file():
try:
ext = filepath.suffix.lower() if filepath.suffix else filepath.stem.lower()
for preview in previews:
for preview in FileRenderer.preview_renderers:
try:
media_type: MediaTypeGroup = getattr(MediaTypes, preview.media_type_name)
if media_type.contains(ext, Context.RENDER):
@@ -30,6 +30,7 @@ logger = structlog.get_logger(__name__)
class AudioPreview(BasePreview):
media_type_name = "audio"
priority = 70
@override
@classmethod
@@ -18,6 +18,7 @@ logger = structlog.get_logger(__name__)
class BlenderPreview(BasePreview):
media_type_name = "blender"
priority = 40
@override
@classmethod
+1
View File
@@ -90,6 +90,7 @@ class CodeStyle(Style):
class CodePreview(BasePreview):
media_type_name = "code"
priority = 60
@override
@classmethod
@@ -23,6 +23,7 @@ logger = structlog.get_logger(__name__)
class EbookPreview(BasePreview):
media_type_name = "ebook"
priority = 40
@override
@classmethod
@@ -21,6 +21,7 @@ logger = structlog.get_logger(__name__)
class RawImagePreview(BasePreview):
media_type_name = "image.raw"
priority = 60
@override
@classmethod
@@ -25,6 +25,7 @@ logger = structlog.get_logger(__name__)
class VectorImagePreview(BasePreview):
media_type_name = "image.vector"
priority = 70
@override
@classmethod
@@ -21,6 +21,7 @@ logger = structlog.get_logger(__name__)
class VideoPreview(BasePreview):
media_type_name = "video"
priority = 70
@override
@classmethod