mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-08-23 04:20:12 +02:00
continue renderer work
This commit is contained in:
+4
-3
@@ -21,15 +21,18 @@ dependencies = [
|
||||
"mutagen~=1.47",
|
||||
"numpy~=2.2",
|
||||
"opencv_python~=4.11",
|
||||
"Pillow>=10.2,<12",
|
||||
"pillow-heif~=1.5.0",
|
||||
"pillow-jxl-plugin~=1.3",
|
||||
"Pillow>=10.2,<12",
|
||||
"py7zr~=1.1.3",
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"Pygments~=2.21",
|
||||
"PySide6==6.11.2",
|
||||
"rarfile==4.2",
|
||||
"rawpy~=0.27",
|
||||
"requests~=2.31.0",
|
||||
"semver~=3.0.4",
|
||||
"Send2Trash>=1.8,<3",
|
||||
"SQLAlchemy~=2.0",
|
||||
"srctools~=2.6",
|
||||
@@ -38,8 +41,6 @@ dependencies = [
|
||||
"typing_extensions~=4.13",
|
||||
"ujson~=5.10",
|
||||
"wcmatch==10.*",
|
||||
"requests~=2.31.0",
|
||||
"semver~=3.0.4",
|
||||
]
|
||||
|
||||
[project.gui-scripts]
|
||||
|
||||
@@ -417,6 +417,7 @@ audio = MediaTypeGroup(
|
||||
]
|
||||
+ midi.types,
|
||||
)
|
||||
MediaTypes.register(audio)
|
||||
|
||||
# RAW Images -----------------------------------------------------------------------------------
|
||||
raw_image = MediaTypeGroup(
|
||||
@@ -608,6 +609,12 @@ markup = MediaTypeGroup(
|
||||
],
|
||||
[SEARCH, RENDER],
|
||||
),
|
||||
_Type(
|
||||
[
|
||||
".plist",
|
||||
],
|
||||
[SEARCH, RENDER],
|
||||
),
|
||||
_Type(".toml", [SEARCH, RENDER]),
|
||||
],
|
||||
)
|
||||
@@ -936,7 +943,6 @@ class MediaCategories:
|
||||
".epub",
|
||||
".fb2",
|
||||
".ibook",
|
||||
".inf",
|
||||
".kfx",
|
||||
".lit",
|
||||
".mobi",
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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.
|
||||
def apply_overlay_color(image: Image, color: UiColor, theme: Theme) -> 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.
|
||||
|
||||
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 theme == Theme.DARK
|
||||
else get_ui_color(ColorType.PRIMARY, color)
|
||||
)
|
||||
fg_color: str = (
|
||||
get_ui_color(ColorType.PRIMARY, color)
|
||||
if theme == Theme.DARK
|
||||
else get_ui_color(ColorType.LIGHT_ACCENT, color)
|
||||
)
|
||||
ol_color: str = (
|
||||
get_ui_color(ColorType.BORDER, color)
|
||||
if theme == Theme.DARK
|
||||
else get_ui_color(ColorType.LIGHT_ACCENT, color)
|
||||
)
|
||||
|
||||
bg: Image = new_image(image.mode, image.size, color=bg_color)
|
||||
fg: Image = new_image(image.mode, image.size, color=fg_color)
|
||||
ol: Image = new_image(image.mode, image.size, color=ol_color)
|
||||
|
||||
bg.paste(fg, (0, 0), mask=image.getchannel(0))
|
||||
bg.paste(ol, (0, 0), mask=image.getchannel(1))
|
||||
|
||||
if image.mode == "RGBA":
|
||||
alpha_bg: Image = bg.copy()
|
||||
alpha_bg.convert("RGBA")
|
||||
alpha_bg.putalpha(0)
|
||||
alpha_bg.paste(bg, (0, 0), mask=image.getchannel(3))
|
||||
bg = alpha_bg
|
||||
|
||||
return bg
|
||||
@@ -27,6 +27,7 @@ from tagstudio.core.media_types import (
|
||||
)
|
||||
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,
|
||||
@@ -35,7 +36,7 @@ from tagstudio.previews.renderers.archive import (
|
||||
open_doc_thumb,
|
||||
powerpoint_thumb,
|
||||
)
|
||||
from tagstudio.previews.renderers.audio import audio_album_thumb, audio_thumb, audio_waveform_thumb
|
||||
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.code import CodePreview
|
||||
@@ -386,7 +387,7 @@ class FileRenderer:
|
||||
)
|
||||
|
||||
# Apply color overlay
|
||||
im = self._apply_overlay_color(im, color, theme)
|
||||
im = apply_overlay_color(im, color, theme)
|
||||
|
||||
return im
|
||||
|
||||
@@ -433,7 +434,7 @@ class FileRenderer:
|
||||
color="#000000",
|
||||
)
|
||||
# Apply color overlay
|
||||
bg = self._apply_overlay_color(im, color, theme)
|
||||
bg = apply_overlay_color(im, color, theme)
|
||||
|
||||
# Paste background color with rounded rectangle mask onto blank image
|
||||
im.paste(
|
||||
@@ -470,48 +471,6 @@ class FileRenderer:
|
||||
|
||||
return im
|
||||
|
||||
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.
|
||||
|
||||
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 theme == Theme.DARK
|
||||
else get_ui_color(ColorType.PRIMARY, color)
|
||||
)
|
||||
fg_color: str = (
|
||||
get_ui_color(ColorType.PRIMARY, color)
|
||||
if theme == Theme.DARK
|
||||
else get_ui_color(ColorType.LIGHT_ACCENT, color)
|
||||
)
|
||||
ol_color: str = (
|
||||
get_ui_color(ColorType.BORDER, color)
|
||||
if theme == Theme.DARK
|
||||
else get_ui_color(ColorType.LIGHT_ACCENT, color)
|
||||
)
|
||||
|
||||
bg: Image.Image = Image.new(image.mode, image.size, color=bg_color)
|
||||
fg: Image.Image = Image.new(image.mode, image.size, color=fg_color)
|
||||
ol: Image.Image = Image.new(image.mode, image.size, color=ol_color)
|
||||
|
||||
bg.paste(fg, (0, 0), mask=image.getchannel(0))
|
||||
bg.paste(ol, (0, 0), mask=image.getchannel(1))
|
||||
|
||||
if image.mode == "RGBA":
|
||||
alpha_bg: Image.Image = bg.copy()
|
||||
alpha_bg.convert("RGBA")
|
||||
alpha_bg.putalpha(0)
|
||||
alpha_bg.paste(bg, (0, 0), mask=image.getchannel(3))
|
||||
bg = alpha_bg
|
||||
|
||||
return bg
|
||||
|
||||
# NOTE: This method will be replaced with frontend specific decorations (Qt painting)
|
||||
def _apply_edge(
|
||||
self,
|
||||
@@ -776,7 +735,7 @@ class FileRenderer:
|
||||
# And allow user-created ones from an external directory.
|
||||
previews: list[type[BasePreview]] = [
|
||||
# ArchivePreview,
|
||||
# AudioPreview,
|
||||
AudioPreview,
|
||||
BlenderPreview,
|
||||
# ClipStudioPaintPreview,
|
||||
CodePreview,
|
||||
@@ -819,216 +778,216 @@ class FileRenderer:
|
||||
|
||||
# -------------------------------- 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()
|
||||
# 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)
|
||||
# # 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
|
||||
# break
|
||||
|
||||
if not image:
|
||||
logger.warning(f"Could not render {ext}")
|
||||
raise NoRendererError
|
||||
# if not image:
|
||||
# logger.warning(f"Could not render {ext}")
|
||||
# raise NoRendererError
|
||||
|
||||
if image:
|
||||
image = self._resize_image(image, (scaled_size, scaled_size))
|
||||
# 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
|
||||
# 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
|
||||
# return image
|
||||
|
||||
# ---------- old old
|
||||
# # ---------- 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 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 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")
|
||||
# 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
|
||||
# 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
|
||||
# return image
|
||||
|
||||
def _resize_image(self, image: Image.Image, size: tuple[int, int]) -> Image.Image:
|
||||
orig_x, orig_y = image.size
|
||||
|
||||
@@ -5,157 +5,170 @@
|
||||
import math
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import override
|
||||
from warnings import catch_warnings
|
||||
|
||||
import numpy as np
|
||||
import structlog
|
||||
from mutagen import flac, id3, mp4
|
||||
from mutagen._util import MutagenError
|
||||
from PIL import Image, ImageDraw
|
||||
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
|
||||
|
||||
from tagstudio.core.enums import Theme
|
||||
from tagstudio.previews.base_preview import BasePreview
|
||||
from tagstudio.previews.effects import apply_overlay_color
|
||||
from tagstudio.previews.vendored.pydub.audio_segment import (
|
||||
_AudioSegment as AudioSegment, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
from tagstudio.qt.views.styles.palette import UiColor
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
|
||||
def audio_thumb(filepath: Path, size: int, dpi_scale: float) -> Image.Image | None:
|
||||
"""Return an album cover preview, or a waveform if cover art does not exist.
|
||||
class AudioPreview(BasePreview):
|
||||
media_type_name = "audio"
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
size (int): The size of the thumbnail.
|
||||
dpi_scale (float): The screen pixel ratio.
|
||||
"""
|
||||
image = audio_album_thumb(filepath)
|
||||
if not image:
|
||||
image = audio_waveform_thumb(filepath, size, dpi_scale)
|
||||
@override
|
||||
@classmethod
|
||||
def render(
|
||||
cls,
|
||||
filepath: Path,
|
||||
theme: Theme,
|
||||
size: tuple[int, int],
|
||||
dpi_scale: float,
|
||||
) -> Image | None:
|
||||
return cls.audio_album_thumb(filepath) or cls.audio_waveform_thumb(
|
||||
filepath, theme, size, dpi_scale
|
||||
)
|
||||
|
||||
return image
|
||||
@staticmethod
|
||||
def audio_album_thumb(filepath: Path) -> Image | None:
|
||||
"""Return an album cover thumb from an audio file if a cover is present.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
image: Image | None = None
|
||||
ext = filepath.suffix.lower()
|
||||
try:
|
||||
if not filepath.is_file():
|
||||
raise FileNotFoundError
|
||||
|
||||
def audio_album_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Return an album cover thumb from an audio file if a cover is present.
|
||||
artwork = None
|
||||
if ext in {".mp3", ".aif", ".aiff"}:
|
||||
id3_tags: id3.ID3 = id3.ID3(filepath)
|
||||
id3_covers: list = id3_tags.getall("APIC") # pyright: ignore[reportUnknownVariableType]
|
||||
if id3_covers:
|
||||
artwork = open_image(BytesIO(id3_covers[0].data))
|
||||
elif ext in {".flac"}:
|
||||
flac_tags: flac.FLAC = flac.FLAC(filepath)
|
||||
flac_covers: list = flac_tags.pictures # pyright: ignore[reportUnknownVariableType]
|
||||
if flac_covers:
|
||||
artwork = open_image(BytesIO(flac_covers[0].data))
|
||||
elif ext in {".mp4", ".m4a", ".aac", ".alac"}:
|
||||
mp4_tags: mp4.MP4 = mp4.MP4(filepath)
|
||||
mp4_covers: list | None = mp4_tags.get("covr") # pyright: ignore[reportUnknownVariableType]
|
||||
if mp4_covers:
|
||||
artwork = open_image(BytesIO(mp4_covers[0]))
|
||||
if artwork:
|
||||
image = artwork
|
||||
except (
|
||||
FileNotFoundError,
|
||||
id3.ID3NoHeaderError,
|
||||
mp4.MP4MetadataError,
|
||||
mp4.MP4StreamInfoError,
|
||||
MutagenError,
|
||||
) as e:
|
||||
logger.error("Couldn't read album artwork", path=filepath, error=type(e).__name__)
|
||||
return image
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
image: Image.Image | None = None
|
||||
ext = filepath.suffix.lower()
|
||||
try:
|
||||
if not filepath.is_file():
|
||||
raise FileNotFoundError
|
||||
@staticmethod
|
||||
def audio_waveform_thumb(
|
||||
filepath: Path, theme: Theme, size: tuple[int, int], dpi_scale: float
|
||||
) -> Image | None:
|
||||
"""Render a waveform image from an audio file.
|
||||
|
||||
artwork = None
|
||||
if ext in {".mp3", ".aif", ".aiff"}:
|
||||
id3_tags: id3.ID3 = id3.ID3(filepath)
|
||||
id3_covers: list = id3_tags.getall("APIC") # pyright: ignore[reportUnknownVariableType]
|
||||
if id3_covers:
|
||||
artwork = Image.open(BytesIO(id3_covers[0].data))
|
||||
elif ext in {".flac"}:
|
||||
flac_tags: flac.FLAC = flac.FLAC(filepath)
|
||||
flac_covers: list = flac_tags.pictures # pyright: ignore[reportUnknownVariableType]
|
||||
if flac_covers:
|
||||
artwork = Image.open(BytesIO(flac_covers[0].data))
|
||||
elif ext in {".mp4", ".m4a", ".aac", ".alac"}:
|
||||
mp4_tags: mp4.MP4 = mp4.MP4(filepath)
|
||||
mp4_covers: list | None = mp4_tags.get("covr") # pyright: ignore[reportUnknownVariableType]
|
||||
if mp4_covers:
|
||||
artwork = Image.open(BytesIO(mp4_covers[0]))
|
||||
if artwork:
|
||||
image = artwork
|
||||
except (
|
||||
FileNotFoundError,
|
||||
id3.ID3NoHeaderError,
|
||||
mp4.MP4MetadataError,
|
||||
mp4.MP4StreamInfoError,
|
||||
MutagenError,
|
||||
) as e:
|
||||
logger.error("Couldn't read album artwork", path=filepath, error=type(e).__name__)
|
||||
return image
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
size (int): The size of the thumbnail.
|
||||
dpi_scale (float): The screen pixel ratio.
|
||||
"""
|
||||
# BASE_SCALE used for drawing on a larger image and resampling down
|
||||
# to provide an antialiased effect.
|
||||
base_scale: int = 2
|
||||
samples_per_bar: int = 3
|
||||
size_scaled: int = size[0] * base_scale # TODO: Allow for non-square sizes
|
||||
allow_small_min: bool = False
|
||||
im: Image | None = None
|
||||
|
||||
try:
|
||||
bar_count: int = min(math.floor((size[0] // dpi_scale) / 5), 64)
|
||||
audio = AudioSegment.from_file(filepath, filepath.suffix.lower()[1:]) # pyright: ignore[reportUnknownVariableType]
|
||||
data = np.frombuffer(buffer=audio._data, dtype=np.int16)
|
||||
data_indices = np.linspace(1, len(data), num=bar_count * samples_per_bar)
|
||||
bar_margin: float = ((size_scaled / (bar_count * 3)) * base_scale) / 2
|
||||
line_width: float = ((size_scaled - bar_margin) / (bar_count * 3)) * base_scale
|
||||
bar_height: float = (size_scaled) - (size_scaled // bar_margin)
|
||||
|
||||
def audio_waveform_thumb(filepath: Path, size: int, dpi_scale: float) -> Image.Image | None:
|
||||
"""Render a waveform image from an audio file.
|
||||
count: int = 0
|
||||
maximum_item: int = 0
|
||||
max_array: list[int] = []
|
||||
highest_line: int = 0
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
size (int): The size of the thumbnail.
|
||||
dpi_scale (float): The screen pixel ratio.
|
||||
"""
|
||||
# BASE_SCALE used for drawing on a larger image and resampling down
|
||||
# to provide an antialiased effect.
|
||||
base_scale: int = 2
|
||||
samples_per_bar: int = 3
|
||||
size_scaled: int = size * base_scale
|
||||
allow_small_min: bool = False
|
||||
im: Image.Image | None = None
|
||||
for i in range(-1, len(data_indices)):
|
||||
d = data[math.ceil(data_indices[i]) - 1]
|
||||
if count < samples_per_bar:
|
||||
count = count + 1
|
||||
with catch_warnings(record=True):
|
||||
if abs(d) > maximum_item:
|
||||
maximum_item = int(abs(d))
|
||||
else:
|
||||
max_array.append(maximum_item)
|
||||
|
||||
try:
|
||||
bar_count: int = min(math.floor((size // dpi_scale) / 5), 64)
|
||||
audio = AudioSegment.from_file(filepath, filepath.suffix.lower()[1:]) # pyright: ignore[reportUnknownVariableType]
|
||||
data = np.frombuffer(buffer=audio._data, dtype=np.int16)
|
||||
data_indices = np.linspace(1, len(data), num=bar_count * samples_per_bar)
|
||||
bar_margin: float = ((size_scaled / (bar_count * 3)) * base_scale) / 2
|
||||
line_width: float = ((size_scaled - bar_margin) / (bar_count * 3)) * base_scale
|
||||
bar_height: float = (size_scaled) - (size_scaled // bar_margin)
|
||||
if maximum_item > highest_line:
|
||||
highest_line = maximum_item
|
||||
|
||||
count: int = 0
|
||||
maximum_item: int = 0
|
||||
max_array: list[int] = []
|
||||
highest_line: int = 0
|
||||
maximum_item = 0
|
||||
count = 1
|
||||
|
||||
for i in range(-1, len(data_indices)):
|
||||
d = data[math.ceil(data_indices[i]) - 1]
|
||||
if count < samples_per_bar:
|
||||
count = count + 1
|
||||
with catch_warnings(record=True):
|
||||
if abs(d) > maximum_item:
|
||||
maximum_item = int(abs(d))
|
||||
else:
|
||||
max_array.append(maximum_item)
|
||||
line_ratio = max(highest_line / bar_height, 1)
|
||||
|
||||
if maximum_item > highest_line:
|
||||
highest_line = maximum_item
|
||||
im = new_image("RGB", (size_scaled, size_scaled), color="#000000")
|
||||
draw = ImageDraw.Draw(im)
|
||||
|
||||
maximum_item = 0
|
||||
count = 1
|
||||
current_x = bar_margin
|
||||
for item in max_array:
|
||||
item_height = item / line_ratio
|
||||
|
||||
line_ratio = max(highest_line / bar_height, 1)
|
||||
# If small minimums are not allowed, raise all values
|
||||
# smaller than the line width to the same value.
|
||||
if not allow_small_min:
|
||||
item_height = max(item_height, line_width)
|
||||
|
||||
im = Image.new("RGB", (size_scaled, size_scaled), color="#000000")
|
||||
draw = ImageDraw.Draw(im)
|
||||
current_y = (bar_height - item_height + (size_scaled // bar_margin)) // 2
|
||||
|
||||
current_x = bar_margin
|
||||
for item in max_array:
|
||||
item_height = item / line_ratio
|
||||
draw.rounded_rectangle(
|
||||
(
|
||||
current_x,
|
||||
current_y,
|
||||
(current_x + line_width),
|
||||
(current_y + item_height),
|
||||
),
|
||||
radius=100 * base_scale,
|
||||
fill=("#FF0000"),
|
||||
outline=("#FFFF00"),
|
||||
width=max(math.ceil(line_width / 6), base_scale),
|
||||
)
|
||||
|
||||
# If small minimums are not allowed, raise all values
|
||||
# smaller than the line width to the same value.
|
||||
if not allow_small_min:
|
||||
item_height = max(item_height, line_width)
|
||||
current_x = current_x + line_width + bar_margin
|
||||
|
||||
current_y = (bar_height - item_height + (size_scaled // bar_margin)) // 2
|
||||
im.resize(size, Resampling.BILINEAR)
|
||||
im = apply_overlay_color(im, UiColor.GREEN, theme)
|
||||
|
||||
draw.rounded_rectangle(
|
||||
(
|
||||
current_x,
|
||||
current_y,
|
||||
(current_x + line_width),
|
||||
(current_y + item_height),
|
||||
),
|
||||
radius=100 * base_scale,
|
||||
fill=("#FF0000"),
|
||||
outline=("#FFFF00"),
|
||||
width=max(math.ceil(line_width / 6), base_scale),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render waveform", path=filepath.name, error=type(e).__name__)
|
||||
|
||||
current_x = current_x + line_width + bar_margin
|
||||
|
||||
im.resize((size, size), Image.Resampling.BILINEAR)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Couldn't render waveform", path=filepath.name, error=type(e).__name__)
|
||||
|
||||
return im
|
||||
return im
|
||||
|
||||
@@ -10,17 +10,9 @@ from typing import override
|
||||
|
||||
import structlog
|
||||
from PIL import 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,
|
||||
)
|
||||
from PIL.Image import DecompressionBombError, Image, Resampling
|
||||
from PIL.Image import new as new_image
|
||||
from PIL.Image import open as open_image
|
||||
from pygments import highlight
|
||||
from pygments.formatters import ImageFormatter
|
||||
from pygments.lexers import PythonLexer
|
||||
@@ -49,19 +41,19 @@ logger = structlog.get_logger(__name__)
|
||||
|
||||
class TextLightStyle(Style):
|
||||
background = "#FFFFFF"
|
||||
foreground = "#111111"
|
||||
foreground = "#000000"
|
||||
|
||||
background_color = background
|
||||
styles = {
|
||||
Generic: foreground,
|
||||
Text: foreground,
|
||||
Literal: foreground,
|
||||
String: foreground,
|
||||
Generic: foreground + " bold",
|
||||
Text: foreground + " bold",
|
||||
Literal: foreground + " bold",
|
||||
String: foreground + " bold",
|
||||
}
|
||||
|
||||
|
||||
class TextDarkStyle(Style):
|
||||
background = "#1e1e1e"
|
||||
background = "#111111"
|
||||
foreground = "#FFFFFF"
|
||||
|
||||
background_color = background
|
||||
@@ -81,10 +73,6 @@ class TextDarkStyle(Style):
|
||||
}
|
||||
|
||||
|
||||
# text_light_style = TextLightStyle()
|
||||
# text_dark_style = TextDarkStyle()
|
||||
|
||||
|
||||
class TextPreview(BasePreview):
|
||||
media_type_name = "plaintext"
|
||||
font = ImageFont.load_default(20)
|
||||
@@ -105,7 +93,6 @@ class TextPreview(BasePreview):
|
||||
)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def text_thumb(
|
||||
filepath: Path,
|
||||
size: tuple[int, int],
|
||||
|
||||
@@ -6,7 +6,9 @@ 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