mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-08-24 21:05:27 +02:00
convert most remaining thumbnail renderers to classes
This commit is contained in:
@@ -10,6 +10,8 @@ from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
from tagstudio.core.utils.sanitized_attr import SanitizedAttr
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
@@ -77,7 +79,7 @@ class MediaTypeGroup:
|
||||
# return False
|
||||
|
||||
|
||||
class MediaTypes:
|
||||
class MediaTypes(metaclass=SanitizedAttr):
|
||||
# TODO: Implement filename equivalencies
|
||||
# TODO: Implement in_all_groups or something, if needed
|
||||
# TODO: Implement collision detection (e.g. ".ts")
|
||||
@@ -165,6 +167,7 @@ clip_studio_paint = MediaTypeGroup(
|
||||
_Type(".cmc", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(clip_studio_paint)
|
||||
|
||||
# Krita ----------------------------------------------------------------------------------------
|
||||
krita = MediaTypeGroup(
|
||||
@@ -174,12 +177,15 @@ krita = MediaTypeGroup(
|
||||
_Type(".krz", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(krita)
|
||||
|
||||
# MediBang Paint / FireAlpaca ------------------------------------------------------------------
|
||||
medibang_paint = MediaTypeGroup("medibang_paint", [_Type(".mdp", [SEARCH, RENDER])])
|
||||
MediaTypes.register(medibang_paint)
|
||||
|
||||
# Paint.NET ------------------------------------------------------------------------------------
|
||||
paint_dot_net = MediaTypeGroup("paint_dot_net", [_Type(".pdn", [SEARCH, RENDER])])
|
||||
MediaTypes.register(paint_dot_net)
|
||||
|
||||
# Archives -------------------------------------------------------------------------------------
|
||||
archive = MediaTypeGroup(
|
||||
@@ -194,6 +200,7 @@ archive = MediaTypeGroup(
|
||||
_Type(".zip", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(archive)
|
||||
|
||||
# eBooks -------------------------------------------------------------------------------------
|
||||
ebook = MediaTypeGroup(
|
||||
@@ -219,6 +226,7 @@ ebook = MediaTypeGroup(
|
||||
_Type(".mscz", RENDER),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(ebook)
|
||||
|
||||
# Fonts --------------------------------------------------------------------------------------
|
||||
font = MediaTypeGroup(
|
||||
@@ -232,6 +240,7 @@ font = MediaTypeGroup(
|
||||
_Type(".woff2", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(font)
|
||||
|
||||
# Office & Documents ---------------------------------------------------------------------------
|
||||
document = MediaTypeGroup(
|
||||
@@ -269,6 +278,7 @@ open_document = MediaTypeGroup(
|
||||
_Type(".ora", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(open_document)
|
||||
|
||||
iwork = MediaTypeGroup(
|
||||
"iwork",
|
||||
@@ -279,16 +289,26 @@ iwork = MediaTypeGroup(
|
||||
_Type(".pxd", RENDER),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(iwork)
|
||||
|
||||
powerpoint = MediaTypeGroup(
|
||||
"office.powerpoint",
|
||||
[
|
||||
_Type(".ppt", SEARCH),
|
||||
_Type(".pptx", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(powerpoint)
|
||||
|
||||
presentation = MediaTypeGroup(
|
||||
"presentation",
|
||||
[
|
||||
_Type(".key", SEARCH),
|
||||
_Type(".odp", SEARCH),
|
||||
_Type(".ppt", SEARCH),
|
||||
_Type(".pptx", [SEARCH, RENDER]),
|
||||
],
|
||||
]
|
||||
+ powerpoint.types,
|
||||
)
|
||||
MediaTypes.register(presentation)
|
||||
|
||||
spreadsheet = MediaTypeGroup(
|
||||
"spreadsheet",
|
||||
@@ -300,6 +320,7 @@ spreadsheet = MediaTypeGroup(
|
||||
_Type(".xlsx", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(spreadsheet)
|
||||
|
||||
# 3D -------------------------------------------------------------------------------------------
|
||||
model = MediaTypeGroup(
|
||||
@@ -311,12 +332,16 @@ model = MediaTypeGroup(
|
||||
_Type(".stl", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(model)
|
||||
|
||||
material = MediaTypeGroup(
|
||||
"material",
|
||||
[
|
||||
_Type(".mtl", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(material)
|
||||
|
||||
shader = MediaTypeGroup(
|
||||
"shader",
|
||||
[
|
||||
@@ -329,6 +354,7 @@ shader = MediaTypeGroup(
|
||||
_Type(".vsh", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(shader)
|
||||
|
||||
# System & Misc ------------------------------------------------------------------------------
|
||||
database = MediaTypeGroup(
|
||||
@@ -342,6 +368,8 @@ database = MediaTypeGroup(
|
||||
_Type(".sqlite3", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(database)
|
||||
|
||||
disk_image = MediaTypeGroup(
|
||||
"disk_image",
|
||||
[
|
||||
@@ -351,6 +379,8 @@ disk_image = MediaTypeGroup(
|
||||
_Type(".iso", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(disk_image)
|
||||
|
||||
installer = MediaTypeGroup(
|
||||
"installer",
|
||||
[
|
||||
@@ -359,6 +389,8 @@ installer = MediaTypeGroup(
|
||||
_Type(".msix", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(installer)
|
||||
|
||||
package = MediaTypeGroup(
|
||||
"package",
|
||||
[
|
||||
@@ -371,6 +403,8 @@ package = MediaTypeGroup(
|
||||
_Type(".xapk", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(package)
|
||||
|
||||
program = MediaTypeGroup(
|
||||
"program",
|
||||
[
|
||||
@@ -379,6 +413,8 @@ program = MediaTypeGroup(
|
||||
_Type(".exe", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(program)
|
||||
|
||||
shortcut = MediaTypeGroup(
|
||||
"shortcut",
|
||||
[
|
||||
@@ -387,15 +423,18 @@ shortcut = MediaTypeGroup(
|
||||
_Type(".url", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(shortcut)
|
||||
|
||||
# Valve Source Engine --------------------------------------------------------------------------
|
||||
source_engine = MediaTypeGroup(
|
||||
"source_engine",
|
||||
[_Type(".vtf", [SEARCH, RENDER])],
|
||||
)
|
||||
MediaTypes.register(source_engine)
|
||||
|
||||
# MIDI -----------------------------------------------------------------------------------------
|
||||
midi = MediaTypeGroup("midi", [_Type([".mid", ".midi"], SEARCH)])
|
||||
MediaTypes.register(midi)
|
||||
|
||||
# Audio ----------------------------------------------------------------------------------------
|
||||
audio = MediaTypeGroup(
|
||||
@@ -439,6 +478,7 @@ raw_image = MediaTypeGroup(
|
||||
_Type(".srf2", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(raw_image)
|
||||
|
||||
# Raster Images --------------------------------------------------------------------------------
|
||||
raster_image = MediaTypeGroup(
|
||||
@@ -469,6 +509,7 @@ raster_image = MediaTypeGroup(
|
||||
_Type([".tif", ".tiff"], [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(raster_image)
|
||||
|
||||
# Vector Images --------------------------------------------------------------------------------
|
||||
vector_image = MediaTypeGroup(
|
||||
@@ -481,6 +522,7 @@ vector_image = MediaTypeGroup(
|
||||
_Type(".svgz", SEARCH),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(vector_image)
|
||||
|
||||
# Animated Images ------------------------------------------------------------------------------
|
||||
animated_image = MediaTypeGroup(
|
||||
@@ -667,6 +709,7 @@ video = MediaTypeGroup(
|
||||
_Type(".wmv", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
MediaTypes.register(video)
|
||||
|
||||
# ------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
class SanitizedAttr(type):
|
||||
def __getattr__(cls, name: str) -> Any: # pyright: ignore[reportExplicitAny]
|
||||
sanitized = name.replace(".", "_")
|
||||
|
||||
if sanitized == name:
|
||||
raise AttributeError(f"'{type(cls).__name__}' object has no attribute '{name}'")
|
||||
|
||||
return getattr(cls, sanitized)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from threading import Lock
|
||||
|
||||
@@ -24,6 +24,7 @@ class BasePreview:
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import new as new_image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
|
||||
from PIL import ImageDraw, ImageFont, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError, Image, Resampling
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
|
||||
|
||||
# TODO: Split out Qt color palette stuff from anything needed by the core.
|
||||
|
||||
@@ -5,17 +5,13 @@
|
||||
import contextlib
|
||||
import hashlib
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from PIL import Image, ImageChops, ImageDraw, ImageEnhance, ImageFile, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
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 (
|
||||
@@ -29,34 +25,27 @@ 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.archive import (
|
||||
apple_embedded_thumb,
|
||||
archive_thumb,
|
||||
krita_thumb,
|
||||
open_doc_thumb,
|
||||
powerpoint_thumb,
|
||||
)
|
||||
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, _blender_thumb
|
||||
from tagstudio.previews.renderers.clip_studio import clip_studio_thumb
|
||||
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 epub_thumb
|
||||
from tagstudio.previews.renderers.font import font_full_preview, font_small_thumb
|
||||
from tagstudio.previews.renderers.medibang_paint import medibang_paint_thumb
|
||||
from tagstudio.previews.renderers.paint_dot_net import paint_dot_net_thumb
|
||||
from tagstudio.previews.renderers.pdf import pdf_thumb
|
||||
from tagstudio.previews.renderers.raster_image import (
|
||||
exr_image_thumb,
|
||||
raster_image_thumb,
|
||||
raw_image_thumb,
|
||||
)
|
||||
from tagstudio.previews.renderers.source_engine import vtf_thumb
|
||||
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,
|
||||
text_thumb,
|
||||
)
|
||||
from tagstudio.previews.renderers.vector_image import vector_image_thumb
|
||||
from tagstudio.previews.renderers.video import video_thumb
|
||||
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,
|
||||
@@ -65,7 +54,7 @@ from tagstudio.qt.app_settings import (
|
||||
)
|
||||
from tagstudio.qt.cache_manager import CacheManager
|
||||
from tagstudio.qt.resource_manager import ResourceManager
|
||||
from tagstudio.qt.views.styles.palette import UI_COLORS, ColorType, UiColor, get_ui_color
|
||||
from tagstudio.qt.views.styles.palette import UI_COLORS, ColorType, UiColor
|
||||
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
@@ -626,7 +615,7 @@ class FileRenderer:
|
||||
size=(thumb_res, thumb_res),
|
||||
dpi_scale=1,
|
||||
theme=theme,
|
||||
is_thumb=is_thumb,
|
||||
is_small=is_thumb,
|
||||
cache_filename=file_name,
|
||||
)
|
||||
|
||||
@@ -707,7 +696,7 @@ class FileRenderer:
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
theme: Theme = Theme.DARK,
|
||||
is_thumb: bool = False,
|
||||
is_small: bool = False,
|
||||
cache_filename: Path | None = None,
|
||||
) -> Image.Image | None:
|
||||
"""Render a thumbnail or preview image.
|
||||
@@ -719,7 +708,7 @@ class FileRenderer:
|
||||
size (tuple[int, int]): The unmodified base size of the thumbnail.
|
||||
dpi_scale (float): The screen pixel ratio.
|
||||
theme (Theme): A theme enum to determine the light/dark theme.
|
||||
is_thumb (bool): Is this specifically a thumbnail? Use for specifying small variants.
|
||||
is_small (bool): Is this specifically a thumbnail? Use for specifying small variants.
|
||||
cache_filename (Path | None): An optional filename to use to save to the cache.
|
||||
|
||||
"""
|
||||
@@ -728,43 +717,51 @@ class FileRenderer:
|
||||
image: Image.Image | None = None
|
||||
is_savable_type: bool = True
|
||||
|
||||
# Ordered groups of file renderers.
|
||||
# A file extension is rendered with the first group it's found in.
|
||||
|
||||
# TODO: Dynamically import these from the renderers/ directory at runtime,
|
||||
# And allow user-created ones from an external directory.
|
||||
previews: list[type[BasePreview]] = [
|
||||
# ArchivePreview,
|
||||
RawImagePreview,
|
||||
VectorImagePreview,
|
||||
RasterImagePreview,
|
||||
VideoPreview,
|
||||
AudioPreview,
|
||||
BlenderPreview,
|
||||
# ClipStudioPaintPreview,
|
||||
CodePreview,
|
||||
# EbookPreview,
|
||||
# FontPreview,
|
||||
# IWorkPreview,
|
||||
# KritaPreview,
|
||||
# MediBangPaintPreview,
|
||||
# PaintDotNetPreview,
|
||||
# RasterImagePreview,
|
||||
# RawImagePreview,
|
||||
TextPreview,
|
||||
# VectorImagePreview,
|
||||
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:
|
||||
media_type: MediaTypeGroup = getattr(MediaTypes, preview.media_type_name)
|
||||
if media_type.contains(ext, Context.RENDER):
|
||||
logger.info(f"{ext} in: {media_type.renderable}")
|
||||
image = preview.render(
|
||||
filepath=filepath,
|
||||
theme=theme,
|
||||
size=(scaled_size, scaled_size),
|
||||
dpi_scale=dpi_scale,
|
||||
try:
|
||||
media_type: MediaTypeGroup = getattr(MediaTypes, preview.media_type_name)
|
||||
if media_type.contains(ext, Context.RENDER):
|
||||
logger.info(f"{ext} in: {media_type.renderable}")
|
||||
image = preview.render(
|
||||
filepath=filepath,
|
||||
is_small=is_small,
|
||||
theme=theme,
|
||||
size=(scaled_size, scaled_size),
|
||||
dpi_scale=dpi_scale,
|
||||
)
|
||||
break
|
||||
except AttributeError:
|
||||
logger.error(
|
||||
f"[FileRenderer] "
|
||||
f"Attribute '{preview.media_type_name}' not registered with MediaTypes",
|
||||
)
|
||||
break
|
||||
|
||||
if image:
|
||||
image = self._resize_image(image, (scaled_size, scaled_size))
|
||||
@@ -776,219 +773,6 @@ class FileRenderer:
|
||||
|
||||
return image
|
||||
|
||||
# -------------------------------- new old
|
||||
|
||||
# render_groups: list[tuple[MediaTypeGroup, Callable[..., Image.Image | None]]] = [
|
||||
# (MediaTypes.raw_image, partial(raw_image_thumb, filepath)),
|
||||
# (MediaTypes.raster_image, partial(raster_image_thumb, filepath)),
|
||||
# (MediaTypes.vector_image, partial(vector_image_thumb, filepath, scaled_size)),
|
||||
# (
|
||||
# getattr(MediaTypes, CodePreview.media_type_name),
|
||||
# partial(CodePreview.render, filepath),
|
||||
# ),
|
||||
# (
|
||||
# getattr(MediaTypes, TextPreview.media_type_name),
|
||||
# partial(TextPreview.render, filepath),
|
||||
# ),
|
||||
# (MediaTypes.audio, partial(audio_thumb, filepath, scaled_size, dpi_scale)),
|
||||
# (MediaTypes.video, partial(video_thumb, filepath)),
|
||||
# (
|
||||
# MediaTypes.font,
|
||||
# partial(font_small_thumb if is_thumb else font_full_preview, filepath, scaled_size),
|
||||
# ),
|
||||
# (MediaTypes.archive, partial(archive_thumb, filepath)),
|
||||
# (MediaTypes.pdf, partial(pdf_thumb, filepath, scaled_size)),
|
||||
# (MediaTypes.ebook, partial(epub_thumb, filepath)),
|
||||
# (MediaTypes.iwork, partial(apple_embedded_thumb, filepath)),
|
||||
# (MediaTypes.blender, partial(_blender_thumb, filepath)),
|
||||
# (MediaTypes.krita, partial(krita_thumb, filepath)),
|
||||
# (MediaTypes.clip_studio_paint, partial(clip_studio_thumb, filepath)),
|
||||
# (MediaTypes.paint_dot_net, partial(paint_dot_net_thumb, filepath)),
|
||||
# (MediaTypes.medibang_paint, partial(medibang_paint_thumb, filepath)),
|
||||
# (MediaTypes.binary, partial(raster_image_thumb, filepath)),
|
||||
# ]
|
||||
# if filepath and filepath.is_file():
|
||||
# try:
|
||||
# ext = filepath.suffix.lower() if filepath.suffix else filepath.stem.lower()
|
||||
# for media_type, renderer in render_groups:
|
||||
# if media_type.contains(ext, Context.RENDER):
|
||||
# logger.info(f"{ext} in: {media_type.renderable}")
|
||||
# image = renderer()
|
||||
|
||||
# # TODO: Remove the need for these extra steps
|
||||
# if image and ext in MediaTypes.audio.renderable:
|
||||
# # TODO: Differentiate between album art and waveform
|
||||
# image = self._apply_overlay_color(image, UiColor.GREEN, theme)
|
||||
# is_savable_type = False
|
||||
# elif image and ext in MediaTypes.font.renderable:
|
||||
# # TODO: Differentiate between ful preview and small preview
|
||||
# image = self._apply_overlay_color(image, UiColor.BLUE, theme)
|
||||
|
||||
# break
|
||||
|
||||
# if not image:
|
||||
# logger.warning(f"Could not render {ext}")
|
||||
# raise NoRendererError
|
||||
|
||||
# if image:
|
||||
# image = self._resize_image(image, (scaled_size, scaled_size))
|
||||
|
||||
# if cache_filename and is_savable_type and image and cache:
|
||||
# cache.save_image(image, cache_filename, mode="RGBA")
|
||||
# except (
|
||||
# AssertionError,
|
||||
# ChildProcessError,
|
||||
# DecompressionBombError,
|
||||
# UnidentifiedImageError,
|
||||
# ValueError,
|
||||
# ) as e:
|
||||
# logger.error(
|
||||
# "[FileRenderer] Couldn't render thumbnail",
|
||||
# filepath=filepath,
|
||||
# error=type(e).__name__,
|
||||
# )
|
||||
# image = None
|
||||
# except NoRendererError:
|
||||
# image = None
|
||||
|
||||
# return image
|
||||
|
||||
# # ---------- old old
|
||||
|
||||
# if filepath and filepath.is_file():
|
||||
# try:
|
||||
# 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 ============================================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.KRITA_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# 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 ==============================================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# image = vtf_thumb(filepath)
|
||||
# # Images ===========================================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.IMAGE_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# # Raw Images -------------------------------------------------------------------
|
||||
# if MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# image = raw_image_thumb(filepath)
|
||||
# # Vector Images ----------------------------------------------------------------
|
||||
# elif ext in MediaTypes.vector_image.renderable:
|
||||
# image = vector_image_thumb(filepath, scaled_size)
|
||||
# # EXR Images -------------------------------------------------------------------
|
||||
# elif ext in [".exr"]:
|
||||
# image = exr_image_thumb(filepath)
|
||||
# # Normal Images ----------------------------------------------------------------
|
||||
# else:
|
||||
# image = raster_image_thumb(filepath)
|
||||
# # Videos ===========================================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# image = video_thumb(filepath)
|
||||
# # PowerPoint =======================================================================
|
||||
# elif ext in {".pptx"}:
|
||||
# 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 + Creator Studio =====================================================
|
||||
# elif (
|
||||
# MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES)
|
||||
# or ext == ".pxd"
|
||||
# ):
|
||||
# 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 ============================================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.FONT_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# if is_thumb:
|
||||
# # Short (Aa) Preview
|
||||
# image = font_small_thumb(filepath, scaled_size)
|
||||
# if image is not None:
|
||||
# image = self._apply_overlay_color(image, UiColor.BLUE, theme)
|
||||
# else:
|
||||
# # Large (Full Alphabet) Preview
|
||||
# image = font_full_preview(filepath, scaled_size)
|
||||
# # Audio ========================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.AUDIO_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# image = audio_album_thumb(filepath, ext)
|
||||
# if image is None:
|
||||
# image = audio_waveform_thumb(filepath, ext, scaled_size, dpi_scale)
|
||||
# is_savable_type = False
|
||||
# if image is not None:
|
||||
# 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)
|
||||
# # PDF ==========================================================
|
||||
# elif MediaCategories.is_ext_in_category(
|
||||
# ext, MediaCategories.PDF_TYPES, mime_fallback=True
|
||||
# ):
|
||||
# image = pdf_thumb(filepath, scaled_size, ext)
|
||||
# # Archives =====================================================
|
||||
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.ARCHIVE_TYPES):
|
||||
# image = archive_thumb(filepath, ext=ext)
|
||||
# # MDIPACK ======================================================
|
||||
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.MDIPACK_TYPES):
|
||||
# image = medibang_paint_thumb(filepath)
|
||||
# # Paint.NET ====================================================
|
||||
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.PAINT_DOT_NET_TYPES):
|
||||
# image = paint_dot_net_thumb(filepath)
|
||||
# # No Rendered Thumbnail ========================================
|
||||
# if not image:
|
||||
# raise NoRendererError
|
||||
|
||||
# if image:
|
||||
# image = self._resize_image(image, (scaled_size, scaled_size))
|
||||
|
||||
# if cache_filename and is_savable_type and image and cache:
|
||||
# cache.save_image(image, cache_filename, mode="RGBA")
|
||||
|
||||
# except (
|
||||
# AssertionError,
|
||||
# ChildProcessError,
|
||||
# DecompressionBombError,
|
||||
# UnidentifiedImageError,
|
||||
# ValueError,
|
||||
# ) as e:
|
||||
# logger.error(
|
||||
# "[FileRenderer] Couldn't render thumbnail",
|
||||
# filepath=filepath,
|
||||
# error=type(e).__name__,
|
||||
# )
|
||||
# image = None
|
||||
# except NoRendererError:
|
||||
# image = None
|
||||
|
||||
# return image
|
||||
|
||||
def _resize_image(self, image: Image.Image, size: tuple[int, int]) -> Image.Image:
|
||||
orig_x, orig_y = image.size
|
||||
new_x, new_y = size
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.archive import archive_thumb
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class AppleEmbeddedPreview(BasePreview):
|
||||
media_type_name = "iwork"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return apple_embedded_thumb(filepath)
|
||||
|
||||
|
||||
def apple_embedded_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio)."""
|
||||
image_names: list[str] = [
|
||||
"preview.jpg",
|
||||
"QuickLook/Preview.heic",
|
||||
"QuickLook/Thumbnail.jpg",
|
||||
"QuickLook/Thumbnail.heic",
|
||||
"QuickLook/Thumbnail.webp",
|
||||
"QuickLook/Icon.webp",
|
||||
]
|
||||
return archive_thumb(filepath, image_names)
|
||||
@@ -6,16 +6,18 @@ import tarfile
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from typing import Literal, override
|
||||
|
||||
import py7zr
|
||||
import py7zr.io
|
||||
import rarfile
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.raster_image import image_from_bytes
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -23,6 +25,22 @@ logger = structlog.get_logger(__name__)
|
||||
type Archive = zipfile.ZipFile | rarfile.RarFile | SevenZipFile | TarFile
|
||||
|
||||
|
||||
class ArchivePreview(BasePreview):
|
||||
media_type_name = "archive"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return archive_thumb(filepath)
|
||||
|
||||
|
||||
class SevenZipFile(py7zr.SevenZipFile):
|
||||
"""Wrapper around py7zr.SevenZipFile to mimic zipfile.ZipFile's API."""
|
||||
|
||||
@@ -81,7 +99,7 @@ def open_archive(filepath: Path) -> Archive:
|
||||
return archiver(filepath, "r")
|
||||
|
||||
|
||||
def first_image_in_archive(archive: Archive) -> Image.Image | None:
|
||||
def first_image_in_archive(archive: Archive) -> Image | None:
|
||||
"""Find and extract the first renderable image in the archive.
|
||||
|
||||
Args:
|
||||
@@ -102,7 +120,7 @@ def first_image_in_archive(archive: Archive) -> Image.Image | None:
|
||||
def archive_thumb(
|
||||
filepath: Path,
|
||||
image_names: list[Path] | list[str] | None = None,
|
||||
) -> Image.Image | None:
|
||||
) -> Image | None:
|
||||
"""Extract an embedded preview image from an archive.
|
||||
|
||||
Args:
|
||||
@@ -131,34 +149,3 @@ def archive_thumb(
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return None
|
||||
|
||||
|
||||
def apple_embedded_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio)."""
|
||||
image_names: list[str] = [
|
||||
"preview.jpg",
|
||||
"QuickLook/Preview.heic",
|
||||
"QuickLook/Thumbnail.jpg",
|
||||
"QuickLook/Thumbnail.heic",
|
||||
"QuickLook/Thumbnail.webp",
|
||||
"QuickLook/Icon.webp",
|
||||
]
|
||||
return archive_thumb(filepath, image_names)
|
||||
|
||||
|
||||
def krita_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for a Krita file."""
|
||||
image_names = ["preview.png"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
|
||||
|
||||
def open_doc_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for an OpenDocument file."""
|
||||
image_names = ["Thumbnails/thumbnail.png"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
|
||||
|
||||
def powerpoint_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for a Microsoft PowerPoint file."""
|
||||
image_names = ["docProps/thumbnail.jpeg"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
|
||||
@@ -12,8 +12,8 @@ import numpy as np
|
||||
import structlog
|
||||
from mutagen import flac, id3, mp4
|
||||
from mutagen._util import MutagenError
|
||||
from PIL import ImageDraw, ImageFont, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError, Image, Resampling
|
||||
from PIL import ImageDraw
|
||||
from PIL.Image import Image, Resampling
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
|
||||
@@ -36,6 +36,7 @@ class AudioPreview(BasePreview):
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
@@ -93,6 +94,7 @@ class AudioPreview(BasePreview):
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
theme (Theme): The system color theme.
|
||||
size (int): The size of the thumbnail.
|
||||
dpi_scale (float): The screen pixel ratio.
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,8 @@ from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import Image, new
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import new as new_image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
@@ -23,6 +24,7 @@ class BlenderPreview(BasePreview):
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
@@ -41,7 +43,7 @@ def _blender_thumb(filepath: Path, theme: Theme) -> Image | None:
|
||||
im: Image | None = None
|
||||
try:
|
||||
if (blend_image := blend_thumb(str(filepath))) is not None:
|
||||
bg = new("RGB", blend_image.size, color=bg_color)
|
||||
bg = new_image("RGB", blend_image.size, color=bg_color)
|
||||
bg.paste(blend_image, mask=blend_image.getchannel(3))
|
||||
im = bg
|
||||
else:
|
||||
|
||||
@@ -5,14 +5,35 @@
|
||||
import sqlite3
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import open as open_image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def clip_studio_thumb(filepath: Path) -> Image.Image | None:
|
||||
class ClipStudioPaintPreview(BasePreview):
|
||||
media_type_name = "clip_studio_paint"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return clip_studio_thumb(filepath)
|
||||
|
||||
|
||||
def clip_studio_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract the thumbnail from the SQLite database embedded in a .clip file.
|
||||
|
||||
Args:
|
||||
@@ -21,7 +42,7 @@ def clip_studio_thumb(filepath: Path) -> Image.Image | None:
|
||||
Returns:
|
||||
Image: The embedded thumbnail, if extractable.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
blob = f.read()
|
||||
@@ -33,7 +54,7 @@ def clip_studio_thumb(filepath: Path) -> Image.Image | None:
|
||||
conn.deserialize(blob[sqlite_index:])
|
||||
thumbnail = conn.execute("SELECT ImageData FROM CanvasPreview").fetchone()
|
||||
if thumbnail:
|
||||
im = Image.open(BytesIO(thumbnail[0]))
|
||||
im = open_image(BytesIO(thumbnail[0]))
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
@@ -6,9 +6,7 @@ from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import (
|
||||
Image,
|
||||
)
|
||||
from PIL.Image import Image
|
||||
from pygments.style import Style
|
||||
from pygments.token import (
|
||||
Comment,
|
||||
@@ -98,6 +96,7 @@ class CodePreview(BasePreview):
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
|
||||
@@ -5,20 +5,39 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.media_types import MediaCategories
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.archive import Archive, first_image_in_archive, open_archive
|
||||
from tagstudio.previews.renderers.raster_image import image_from_bytes
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def epub_thumb(filepath: Path) -> Image.Image | None:
|
||||
class EbookPreview(BasePreview):
|
||||
media_type_name = "ebook"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return epub_thumb(filepath)
|
||||
|
||||
|
||||
def epub_thumb(filepath: Path) -> Image | None:
|
||||
"""Extracts the cover specified by ComicInfo.xml or first image found in the ePub file.
|
||||
|
||||
Args:
|
||||
@@ -29,14 +48,14 @@ def epub_thumb(filepath: Path) -> Image.Image | None:
|
||||
Image: The cover specified in ComicInfo.xml,
|
||||
the first image found in the ePub file, or None by default.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
try:
|
||||
with open_archive(filepath) as archive:
|
||||
if "ComicInfo.xml" in archive.namelist():
|
||||
comic_info = ET.fromstring(archive.read("ComicInfo.xml"))
|
||||
im = _cover_from_comic_info(archive, comic_info, "FrontCover")
|
||||
im = cover_from_comic_info(archive, comic_info, "FrontCover")
|
||||
if not im:
|
||||
im = _cover_from_comic_info(archive, comic_info, "InnerCover")
|
||||
im = cover_from_comic_info(archive, comic_info, "InnerCover")
|
||||
|
||||
if not im:
|
||||
im = first_image_in_archive(archive)
|
||||
@@ -46,9 +65,7 @@ def epub_thumb(filepath: Path) -> Image.Image | None:
|
||||
return im
|
||||
|
||||
|
||||
def _cover_from_comic_info(
|
||||
archive: Archive, comic_info: Element, cover_type: str
|
||||
) -> Image.Image | None:
|
||||
def cover_from_comic_info(archive: Archive, comic_info: Element, cover_type: str) -> Image | None:
|
||||
"""Extract the cover specified in ComicInfo.xml.
|
||||
|
||||
Args:
|
||||
@@ -59,7 +76,7 @@ def _cover_from_comic_info(
|
||||
Returns:
|
||||
Image: The cover specified in ComicInfo.xml.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
|
||||
cover = comic_info.find(f"./*Page[@Type='{cover_type}']")
|
||||
if cover is not None:
|
||||
|
||||
@@ -4,37 +4,65 @@
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
from typing import cast, override
|
||||
|
||||
import numpy as np
|
||||
import structlog
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from PIL import ImageDraw, ImageFont
|
||||
from PIL.Image import Image, Resampling, fromarray
|
||||
from PIL.Image import new as new_image
|
||||
|
||||
from tagstudio.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.effects import apply_overlay_color
|
||||
from tagstudio.qt.helpers.text_wrapper import wrap_full_text
|
||||
from tagstudio.qt.views.styles.color_overlay import auto_theme_overlay
|
||||
from tagstudio.qt.views.styles.palette import UiColor
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def font_small_thumb(filepath: Path, size: int) -> Image.Image | None:
|
||||
class FontPreview(BasePreview):
|
||||
media_type_name = "font"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return (
|
||||
font_small_thumb(filepath, theme, size)
|
||||
if is_small
|
||||
else font_full_preview(filepath, size)
|
||||
)
|
||||
|
||||
|
||||
def font_small_thumb(filepath: Path, theme: Theme, size: tuple[int, int]) -> Image | None:
|
||||
"""Render a small font preview ("Aa") thumbnail from a font file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
theme (Theme): The system color theme.
|
||||
size (tuple[int,int]): The size of the thumbnail.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
# TODO: Support for non-square images
|
||||
im: Image | None = None
|
||||
try:
|
||||
bg = Image.new("RGB", (size, size), color="#000000")
|
||||
raw = Image.new("RGB", (size * 3, size * 3), color="#000000")
|
||||
bg = new_image("RGB", size, color="#000000")
|
||||
raw = new_image("RGB", (size[0] * 3, size[1] * 3), color="#000000")
|
||||
draw = ImageDraw.Draw(raw)
|
||||
font = ImageFont.truetype(filepath, size=size)
|
||||
font = ImageFont.truetype(filepath, size=size[0])
|
||||
# NOTE: While a stroke effect is desired, the text
|
||||
# method only allows for outer strokes, which looks
|
||||
# a bit weird when rendering fonts.
|
||||
draw.text(
|
||||
(size // 8, size // 8),
|
||||
(size[0] // 8, size[1] // 8),
|
||||
"Aa",
|
||||
font=font,
|
||||
fill="#FF0000",
|
||||
@@ -51,34 +79,37 @@ def font_small_thumb(filepath: Path, size: int) -> Image.Image | None:
|
||||
row.argmax() : m - row[::-1].argmax(),
|
||||
col.argmax() : n - col[::-1].argmax(),
|
||||
]
|
||||
cropped_im: Image.Image = Image.fromarray(cropped_data, "RGB")
|
||||
cropped_im: Image = fromarray(cropped_data, "RGB")
|
||||
|
||||
margin: int = math.ceil(size // 16)
|
||||
margin: int = math.ceil(size[0] // 16)
|
||||
|
||||
orig_x, orig_y = cropped_im.size
|
||||
new_x, new_y = (size, size)
|
||||
new_x, new_y = size
|
||||
if orig_x > orig_y:
|
||||
new_x = size
|
||||
new_y = math.ceil(size * (orig_y / orig_x))
|
||||
new_x = size[0]
|
||||
new_y = math.ceil(size[1] * (orig_y / orig_x))
|
||||
elif orig_y > orig_x:
|
||||
new_y = size
|
||||
new_x = math.ceil(size * (orig_x / orig_y))
|
||||
new_y = size[1]
|
||||
new_x = math.ceil(size[0] * (orig_x / orig_y))
|
||||
|
||||
cropped_im = cropped_im.resize(
|
||||
size=(new_x - (margin * 2), new_y - (margin * 2)),
|
||||
resample=Image.Resampling.BILINEAR,
|
||||
resample=Resampling.BILINEAR,
|
||||
)
|
||||
bg.paste(
|
||||
cropped_im,
|
||||
box=(margin, margin + ((size - new_y) // 2)),
|
||||
box=(margin, margin + ((size[1] - new_y) // 2)),
|
||||
)
|
||||
im = bg
|
||||
im = apply_overlay_color(im, UiColor.BLUE, theme)
|
||||
|
||||
except OSError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
return im
|
||||
|
||||
|
||||
def font_full_preview(filepath: Path, size: int) -> Image.Image | None:
|
||||
def font_full_preview(filepath: Path, size: tuple[int, int]) -> Image | None:
|
||||
"""Render a large font preview ("Alphabet") thumbnail from a font file.
|
||||
|
||||
Args:
|
||||
@@ -87,21 +118,25 @@ def font_full_preview(filepath: Path, size: int) -> Image.Image | None:
|
||||
"""
|
||||
# Scale the sample font sizes to the preview image
|
||||
# resolution,assuming the sizes are tuned for 256px.
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
# TODO: Support for non-square images
|
||||
try:
|
||||
scaled_sizes: list[int] = [math.floor(x * (size / 256)) for x in FONT_SAMPLE_SIZES]
|
||||
bg = Image.new("RGBA", (size, size), color="#00000000")
|
||||
scaled_sizes: list[int] = [math.floor(x * (size[0] / 256)) for x in FONT_SAMPLE_SIZES]
|
||||
bg = new_image("RGBA", size, color="#00000000")
|
||||
draw = ImageDraw.Draw(bg)
|
||||
lines_of_padding = 2
|
||||
y_offset = 0.0
|
||||
|
||||
for font_size in scaled_sizes:
|
||||
font = ImageFont.truetype(filepath, size=font_size)
|
||||
text_wrapped: str = wrap_full_text(FONT_SAMPLE_TEXT, font=font, width=size, draw=draw)
|
||||
text_wrapped: str = wrap_full_text(
|
||||
FONT_SAMPLE_TEXT, font=font, width=size[0], draw=draw
|
||||
)
|
||||
draw.multiline_text((0, y_offset), text_wrapped, font=font)
|
||||
y_offset += (len(text_wrapped.split("\n")) + lines_of_padding) * draw.textbbox(
|
||||
(0, 0), "A", font=font
|
||||
)[-1]
|
||||
# TODO: Separate from any Qt stuff
|
||||
im = auto_theme_overlay(bg, use_alpha=False)
|
||||
except OSError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.archive import archive_thumb
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class KritaPreview(BasePreview):
|
||||
media_type_name = "krita"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return krita_thumb(filepath)
|
||||
|
||||
|
||||
def krita_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract and render a thumbnail for a Krita file."""
|
||||
image_names = ["preview.png"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
@@ -7,16 +7,35 @@ import struct
|
||||
import xml.etree.ElementTree as ET
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image, frombytes
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def medibang_paint_thumb(filepath: Path) -> Image.Image | None:
|
||||
class MediBangPaintPreview(BasePreview):
|
||||
media_type_name = "medibang_paint"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return medibang_paint_thumb(filepath)
|
||||
|
||||
|
||||
def medibang_paint_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract the thumbnail from a .mdp file.
|
||||
|
||||
Args:
|
||||
@@ -25,7 +44,7 @@ def medibang_paint_thumb(filepath: Path) -> Image.Image | None:
|
||||
Returns:
|
||||
Image: The embedded thumbnail.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
magic = struct.unpack("<7sx", f.read(8))[0]
|
||||
@@ -50,7 +69,7 @@ def medibang_paint_thumb(filepath: Path) -> Image.Image | None:
|
||||
if pac_header[2] == 1:
|
||||
thumb_blob = zlib.decompress(thumb_blob, bufsize=pac_header[4])
|
||||
|
||||
im = Image.frombytes("RGBA", dimensions, thumb_blob, "raw", "BGRA")
|
||||
im = frombytes("RGBA", dimensions, thumb_blob, "raw", "BGRA")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.archive import archive_thumb
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class OpenDocumentPreview(BasePreview):
|
||||
media_type_name = "open_document"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return open_doc_thumb(filepath)
|
||||
|
||||
|
||||
def open_doc_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract and render a thumbnail for an OpenDocument file."""
|
||||
image_names = ["Thumbnails/thumbnail.png"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
@@ -7,14 +7,36 @@ import struct
|
||||
import xml.etree.ElementTree as ET
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def paint_dot_net_thumb(filepath: Path) -> Image.Image | None:
|
||||
class PaintDotNetPreview(BasePreview):
|
||||
media_type_name = "paint_dot_net"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return paint_dot_net_thumb(filepath)
|
||||
|
||||
|
||||
def paint_dot_net_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract the base64-encoded thumbnail from a .pdn file header.
|
||||
|
||||
Args:
|
||||
@@ -23,7 +45,7 @@ def paint_dot_net_thumb(filepath: Path) -> Image.Image | None:
|
||||
Returns:
|
||||
Image: the decoded PNG thumbnail or None by default.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
with open(filepath, "rb") as f:
|
||||
try:
|
||||
# First 4 bytes are the magic number
|
||||
@@ -39,9 +61,9 @@ def paint_dot_net_thumb(filepath: Path) -> Image.Image | None:
|
||||
encoded_png = thumb_element.get("png")
|
||||
if encoded_png:
|
||||
decoded_png = base64.b64decode(encoded_png)
|
||||
im = Image.open(BytesIO(decoded_png))
|
||||
im = open_image(BytesIO(decoded_png))
|
||||
if im.mode == "RGBA":
|
||||
new_bg = Image.new("RGB", im.size, color="#1e1e1e")
|
||||
new_bg = new_image("RGB", im.size, color="#1e1e1e")
|
||||
new_bg.paste(im, mask=im.getchannel(3))
|
||||
im = new_bg
|
||||
except Exception as e:
|
||||
|
||||
@@ -7,26 +7,46 @@
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import Image
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import open as open_image
|
||||
from PySide6.QtCore import QBuffer, QFile, QFileDevice, QIODeviceBase, QSizeF
|
||||
from PySide6.QtGui import QImage
|
||||
from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.qt.views.styles.image_effects import replace_transparent_pixels
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def pdf_thumb(filepath: Path, size: int) -> Image.Image | None:
|
||||
class PdfPreview(BasePreview):
|
||||
media_type_name = "pdf"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return pdf_thumb(filepath, size)
|
||||
|
||||
|
||||
def pdf_thumb(filepath: Path, size: tuple[int, int]) -> Image | None:
|
||||
"""Render a thumbnail for a PDF or Adobe Illustrator file.
|
||||
|
||||
filepath (Path): The path of the file.
|
||||
size (int): The size of the icon.
|
||||
ext (str): The file extension.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
|
||||
file: QFile = QFile(filepath)
|
||||
success: bool = file.open(QIODeviceBase.OpenModeFlag.ReadOnly, QFileDevice.Permission.ReadUser)
|
||||
@@ -39,10 +59,11 @@ def pdf_thumb(filepath: Path, size: int) -> Image.Image | None:
|
||||
# Transform page_size in points to pixels with proper aspect ratio
|
||||
page_size: QSizeF = document.pagePointSize(0)
|
||||
ratio_hw: float = page_size.height() / page_size.width()
|
||||
# TODO: Make compatible with non-square images
|
||||
if ratio_hw >= 1:
|
||||
page_size *= size / page_size.height()
|
||||
page_size *= size[0] / page_size.height()
|
||||
else:
|
||||
page_size *= size / page_size.width()
|
||||
page_size *= size[0] / page_size.width()
|
||||
# Enlarge image for anti-aliasing
|
||||
scale_factor = 2.5
|
||||
page_size *= scale_factor
|
||||
@@ -59,7 +80,7 @@ def pdf_thumb(filepath: Path, size: int) -> Image.Image | None:
|
||||
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
try:
|
||||
q_image.save(buffer, "PNG") # pyright: ignore
|
||||
im = Image.open(BytesIO(buffer.buffer().data()))
|
||||
im = open_image(BytesIO(buffer.buffer().data()))
|
||||
finally:
|
||||
buffer.close()
|
||||
# Replace transparent pixels with white (otherwise Background defaults to transparent)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL.Image import Image
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.renderers.archive import archive_thumb
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class PowerPointPreview(BasePreview):
|
||||
media_type_name = "office.powerpoint"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return powerpoint_thumb(filepath)
|
||||
|
||||
|
||||
def powerpoint_thumb(filepath: Path) -> Image | None:
|
||||
"""Extract and render a thumbnail for a Microsoft PowerPoint file."""
|
||||
image_names = ["docProps/thumbnail.jpeg"]
|
||||
return archive_thumb(filepath, image_names)
|
||||
@@ -5,20 +5,20 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL import Image, ImageOps, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PIL import ImageOps, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError, Image, fromarray
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
from pillow_heif import register_heif_opener # pyright: ignore[reportUnknownVariableType]
|
||||
from rawpy import (
|
||||
LibRawFileUnsupportedError, # pyright: ignore[reportPrivateImportUsage]
|
||||
LibRawIOError, # pyright: ignore[reportPrivateImportUsage]
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.core.utils.types import unwrap
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
@@ -31,13 +31,29 @@ register_heif_opener()
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
|
||||
|
||||
def raster_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
class RasterImagePreview(BasePreview):
|
||||
media_type_name = "image.raster"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return raster_image_thumb(filepath)
|
||||
|
||||
|
||||
def raster_image_thumb(filepath: Path) -> Image | None:
|
||||
"""Render a thumbnail for a standard image type.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
try:
|
||||
if filepath.suffix.lower() == ".exr":
|
||||
return exr_image_thumb(filepath)
|
||||
@@ -55,13 +71,13 @@ def raster_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
return im
|
||||
|
||||
|
||||
def exr_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
def exr_image_thumb(filepath: Path) -> Image | None:
|
||||
"""Render a thumbnail for a EXR image type.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
try:
|
||||
# Load the EXR data to an array and rotate the color space from BGRA -> RGBA
|
||||
raw_array = cv2.imread(str(filepath), cv2.IMREAD_UNCHANGED)
|
||||
@@ -73,11 +89,11 @@ def exr_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
array_gamma = np.power(np.clip(raw_array, 0, 1), 1 / gamma)
|
||||
array = (array_gamma * 255).astype(np.uint8)
|
||||
|
||||
im = Image.fromarray(array, mode="RGBA")
|
||||
im = fromarray(array, mode="RGBA")
|
||||
|
||||
# Paste solid background
|
||||
if im.mode == "RGBA":
|
||||
new_bg = Image.new("RGB", im.size, color="#1e1e1e")
|
||||
new_bg = new_image("RGB", im.size, color="#1e1e1e")
|
||||
new_bg.paste(im, mask=im.getchannel(3))
|
||||
im = new_bg
|
||||
|
||||
@@ -86,32 +102,7 @@ def exr_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
return im
|
||||
|
||||
|
||||
def raw_image_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Render a thumbnail for a RAW image type.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
try:
|
||||
with rawpy.imread(str(filepath)) as raw:
|
||||
rgb = raw.postprocess(use_camera_wb=True)
|
||||
im = Image.frombytes(
|
||||
"RGB",
|
||||
(rgb.shape[1], rgb.shape[0]),
|
||||
rgb,
|
||||
decoder_name="raw",
|
||||
)
|
||||
except (
|
||||
DecompressionBombError,
|
||||
LibRawFileUnsupportedError,
|
||||
LibRawIOError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
|
||||
def image_from_bytes(image_data: BytesIO) -> Image.Image:
|
||||
def image_from_bytes(image_data: BytesIO) -> Image:
|
||||
"""Load a raster image and add a background if it's transparent.
|
||||
|
||||
Args:
|
||||
@@ -120,11 +111,11 @@ def image_from_bytes(image_data: BytesIO) -> Image.Image:
|
||||
Returns:
|
||||
Image.Image: The loaded raster image, with a background if needed.
|
||||
"""
|
||||
im: Image.Image = Image.open(image_data)
|
||||
im: Image = open_image(image_data)
|
||||
if im.mode != "RGB" and im.mode != "RGBA":
|
||||
im = im.convert(mode="RGBA")
|
||||
if im.mode == "RGBA":
|
||||
new_bg = Image.new("RGB", im.size, color="#1e1e1e")
|
||||
new_bg = new_image("RGB", im.size, color="#1e1e1e")
|
||||
new_bg.paste(im, mask=im.getchannel(3))
|
||||
im = new_bg
|
||||
return unwrap(ImageOps.exif_transpose(im))
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL.Image import DecompressionBombError, Image, frombytes
|
||||
from rawpy import (
|
||||
LibRawFileUnsupportedError, # pyright: ignore[reportPrivateImportUsage]
|
||||
LibRawIOError, # pyright: ignore[reportPrivateImportUsage]
|
||||
)
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
class RawImagePreview(BasePreview):
|
||||
media_type_name = "image.raw"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return raw_image_thumb(filepath)
|
||||
|
||||
|
||||
def raw_image_thumb(filepath: Path) -> Image | None:
|
||||
"""Render a thumbnail for a RAW image type.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image | None = None
|
||||
try:
|
||||
with rawpy.imread(str(filepath)) as raw:
|
||||
rgb = raw.postprocess(use_camera_wb=True)
|
||||
im = frombytes(
|
||||
"RGB",
|
||||
(rgb.shape[1], rgb.shape[0]),
|
||||
rgb,
|
||||
decoder_name="raw",
|
||||
)
|
||||
except (
|
||||
DecompressionBombError,
|
||||
LibRawFileUnsupportedError,
|
||||
LibRawIOError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
@@ -82,6 +82,7 @@ class TextPreview(BasePreview):
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
|
||||
@@ -6,29 +6,49 @@
|
||||
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import (
|
||||
Image,
|
||||
UnidentifiedImageError,
|
||||
)
|
||||
from PIL import UnidentifiedImageError
|
||||
from PIL.Image import Image
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
from PySide6.QtCore import QBuffer, Qt
|
||||
from PySide6.QtGui import QImage, QPainter
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def vector_image_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
class VectorImagePreview(BasePreview):
|
||||
media_type_name = "image.vector"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return vector_image_thumb(filepath, size)
|
||||
|
||||
|
||||
def vector_image_thumb(filepath: Path, size: tuple[int, int]) -> Image:
|
||||
"""Render a thumbnail for a vector image, such as SVG.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
size (tuple[int,int]): The size of the thumbnail.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
# Create an image to draw the svg to and a painter to do the drawing
|
||||
q_image: QImage = QImage(size, size, QImage.Format.Format_ARGB32)
|
||||
q_image: QImage = QImage(size[0], size[1], QImage.Format.Format_ARGB32)
|
||||
q_image.fill("#1e1e1e")
|
||||
|
||||
# Create an svg renderer, then render to the painter
|
||||
@@ -48,8 +68,8 @@ def vector_image_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
q_image.save(buffer, "PNG") # pyright: ignore[reportCallIssue, reportArgumentType]
|
||||
|
||||
# Load the image from the buffer
|
||||
im = Image.new("RGB", (size, size), color="#1e1e1e")
|
||||
im.paste(Image.open(BytesIO(buffer.data().data())))
|
||||
im = new_image("RGB", size, color="#1e1e1e")
|
||||
im.paste(open_image(BytesIO(buffer.data().data())))
|
||||
im = im.convert(mode="RGB")
|
||||
|
||||
buffer.close()
|
||||
|
||||
@@ -4,25 +4,44 @@
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
|
||||
import cv2
|
||||
import structlog
|
||||
from cv2.typing import MatLike
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PIL import UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError, Image, fromarray
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.video_tester import is_readable_video
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def video_thumb(filepath: Path) -> Image.Image | None:
|
||||
class VideoPreview(BasePreview):
|
||||
media_type_name = "video"
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
is_small: bool,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return video_thumb(filepath)
|
||||
|
||||
|
||||
def video_thumb(filepath: Path) -> Image | None:
|
||||
"""Render a thumbnail for a video file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
im: Image | None = None
|
||||
frame: MatLike | None = None
|
||||
try:
|
||||
if is_readable_video(filepath):
|
||||
@@ -49,7 +68,12 @@ def video_thumb(filepath: Path) -> Image.Image | None:
|
||||
break
|
||||
if frame is not None:
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
im = Image.fromarray(frame)
|
||||
except (UnidentifiedImageError, cv2.error, DecompressionBombError, OSError) as e:
|
||||
im = fromarray(frame)
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
cv2.error,
|
||||
DecompressionBombError,
|
||||
OSError,
|
||||
) as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@@ -83,6 +83,8 @@ def classproperty(func):
|
||||
AUDIO_FILE_EXT_ALIASES = {
|
||||
"m4a": "mp4",
|
||||
"wave": "wav",
|
||||
"aif": "aiff",
|
||||
"aifc": "aiff",
|
||||
}
|
||||
|
||||
WavSubChunk = namedtuple("WavSubChunk", ["id", "position", "size"])
|
||||
|
||||
@@ -6,9 +6,7 @@ from PIL import Image
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.gradients import linear_gradient
|
||||
from tagstudio.qt.views.styles.palette import ColorType, UiColor, get_ui_color
|
||||
|
||||
# TODO: Consolidate the built-in QT theme values with the values
|
||||
# here, in enums.py, and in palette.py.
|
||||
|
||||
Reference in New Issue
Block a user