mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-02-02 16:19:10 +00:00
feat(ui): add more default icons and file type equivalencies (#882)
* feat(ui): expand file and thumbnail support * feat: add iwork and powerpoint thumb support Note: a lot of the zip-based code is becoming duplicated - this should be consolidated in the future. * fix: remove decompression bomb check and catch others * feat: add .aiff file equivalencies * ui: update database icon * feat: add .effect and .shader to shader set * fix: correct malformed or missing media types * feat: add misc code/plaintext types to media types * fix: catch BadZipFile error for iWork thumbs * chore: add type hints to thumb_renderer dicts * refactor: change most internal render methods to static
This commit is contained in:
committed by
GitHub
parent
33e6bc180d
commit
d7d7e21d13
@@ -1,7 +1,8 @@
|
||||
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
|
||||
# Copyright (C) 2025 Travis Abendshien (CyanVoxel).
|
||||
# Licensed under the GPL-3.0 License.
|
||||
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
|
||||
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
from dataclasses import dataclass
|
||||
@@ -10,7 +11,16 @@ from pathlib import Path
|
||||
|
||||
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
||||
|
||||
FILETYPE_EQUIVALENTS = [set(["jpg", "jpeg"])]
|
||||
FILETYPE_EQUIVALENTS = [
|
||||
set(["aif", "aiff", "aifc"]),
|
||||
set(["html", "htm", "xhtml", "shtml", "dhtml"]),
|
||||
set(["jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"]),
|
||||
set(["json", "jsonc", "json5"]),
|
||||
set(["md", "markdown", "mkd", "rmd"]),
|
||||
set(["tar.gz", "tgz"]),
|
||||
set(["xml", "xul"]),
|
||||
set(["yaml", "yml"]),
|
||||
]
|
||||
|
||||
|
||||
class MediaType(str, Enum):
|
||||
@@ -22,6 +32,7 @@ class MediaType(str, Enum):
|
||||
AUDIO_MIDI = "audio_midi"
|
||||
AUDIO = "audio"
|
||||
BLENDER = "blender"
|
||||
CODE = "code"
|
||||
DATABASE = "database"
|
||||
DISK_IMAGE = "disk_image"
|
||||
DOCUMENT = "document"
|
||||
@@ -32,6 +43,7 @@ class MediaType(str, Enum):
|
||||
IMAGE_VECTOR = "image_vector"
|
||||
IMAGE = "image"
|
||||
INSTALLER = "installer"
|
||||
IWORK = "iwork"
|
||||
MATERIAL = "material"
|
||||
MODEL = "model"
|
||||
OPEN_DOCUMENT = "open_document"
|
||||
@@ -40,6 +52,7 @@ class MediaType(str, Enum):
|
||||
PLAINTEXT = "plaintext"
|
||||
PRESENTATION = "presentation"
|
||||
PROGRAM = "program"
|
||||
SHADER = "shader"
|
||||
SHORTCUT = "shortcut"
|
||||
SOURCE_ENGINE = "source_engine"
|
||||
SPREADSHEET = "spreadsheet"
|
||||
@@ -96,6 +109,7 @@ class MediaCategories:
|
||||
_AUDIO_SET: set[str] = {
|
||||
".aac",
|
||||
".aif",
|
||||
".aifc",
|
||||
".aiff",
|
||||
".alac",
|
||||
".flac",
|
||||
@@ -143,9 +157,71 @@ class MediaCategories:
|
||||
".blend31",
|
||||
".blend32",
|
||||
}
|
||||
_CODE_SET: set[str] = {
|
||||
".bat",
|
||||
".cfg",
|
||||
".conf",
|
||||
".cpp",
|
||||
".cs",
|
||||
".csh",
|
||||
".css",
|
||||
".d",
|
||||
".dhtml",
|
||||
".fgd",
|
||||
".fish",
|
||||
".gitignore",
|
||||
".h",
|
||||
".hpp",
|
||||
".htm",
|
||||
".html",
|
||||
".inf",
|
||||
".ini",
|
||||
".js",
|
||||
".json",
|
||||
".json5",
|
||||
".jsonc",
|
||||
".jsx",
|
||||
".kv3",
|
||||
".lua",
|
||||
".meta",
|
||||
".nix",
|
||||
".nu",
|
||||
".nut",
|
||||
".php",
|
||||
".plist",
|
||||
".prefs",
|
||||
".ps1",
|
||||
".py",
|
||||
".pyi",
|
||||
".qml",
|
||||
".qrc",
|
||||
".qss",
|
||||
".rs",
|
||||
".sh",
|
||||
".shtml",
|
||||
".sip",
|
||||
".spec",
|
||||
".tcl",
|
||||
".timestamp",
|
||||
".toml",
|
||||
".ts",
|
||||
".tsx",
|
||||
".vcfg",
|
||||
".vdf",
|
||||
".vmt",
|
||||
".vqlayout",
|
||||
".vsc",
|
||||
".vsnd_template",
|
||||
".xhtml",
|
||||
".xml",
|
||||
".xul",
|
||||
".yaml",
|
||||
".yml",
|
||||
}
|
||||
_DATABASE_SET: set[str] = {
|
||||
".accdb",
|
||||
".mdb",
|
||||
".pdb",
|
||||
".sqlite",
|
||||
".sqlite3",
|
||||
}
|
||||
@@ -166,23 +242,23 @@ class MediaCategories:
|
||||
".wps",
|
||||
}
|
||||
_EBOOK_SET: set[str] = {
|
||||
".azw",
|
||||
".azw3",
|
||||
".cb7",
|
||||
".cba",
|
||||
".cbr",
|
||||
".cbt",
|
||||
".cbz",
|
||||
".djvu",
|
||||
".epub",
|
||||
# ".azw",
|
||||
# ".azw3",
|
||||
# ".cb7",
|
||||
# ".cba",
|
||||
# ".cbr",
|
||||
# ".cbt",
|
||||
# ".cbz",
|
||||
# ".djvu",
|
||||
# ".fb2",
|
||||
# ".ibook",
|
||||
# ".inf",
|
||||
# ".kfx",
|
||||
# ".lit",
|
||||
# ".mobi",
|
||||
# ".pdb"
|
||||
# ".prc",
|
||||
".fb2",
|
||||
".ibook",
|
||||
".inf",
|
||||
".kfx",
|
||||
".lit",
|
||||
".mobi",
|
||||
".pdb",
|
||||
".prc",
|
||||
}
|
||||
_FONT_SET: set[str] = {
|
||||
".fon",
|
||||
@@ -209,7 +285,7 @@ class MediaCategories:
|
||||
".raw",
|
||||
".rw2",
|
||||
}
|
||||
_IMAGE_VECTOR_SET: set[str] = {".svg"}
|
||||
_IMAGE_VECTOR_SET: set[str] = {".eps", ".epsf", ".epsi", ".svg", ".svgz"}
|
||||
_IMAGE_RASTER_SET: set[str] = {
|
||||
".apng",
|
||||
".avif",
|
||||
@@ -218,6 +294,7 @@ class MediaCategories:
|
||||
".gif",
|
||||
".heic",
|
||||
".heif",
|
||||
".icns",
|
||||
".j2k",
|
||||
".jfif",
|
||||
".jp2",
|
||||
@@ -235,6 +312,7 @@ class MediaCategories:
|
||||
".webp",
|
||||
}
|
||||
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
|
||||
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
|
||||
_MATERIAL_SET: set[str] = {".mtl"}
|
||||
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
|
||||
_OPEN_DOCUMENT_SET: set[str] = {
|
||||
@@ -258,51 +336,21 @@ class MediaCategories:
|
||||
".pkg",
|
||||
".xapk",
|
||||
}
|
||||
_PDF_SET: set[str] = {
|
||||
".pdf",
|
||||
}
|
||||
_PDF_SET: set[str] = {".pdf"}
|
||||
_PLAINTEXT_SET: set[str] = {
|
||||
".bat",
|
||||
".cfg",
|
||||
".conf",
|
||||
".cpp",
|
||||
".cs",
|
||||
".css",
|
||||
".csv",
|
||||
".fgd",
|
||||
".gi",
|
||||
".h",
|
||||
".hpp",
|
||||
".htm",
|
||||
".html",
|
||||
".inf",
|
||||
".ini",
|
||||
".js",
|
||||
".json",
|
||||
".jsonc",
|
||||
".kv3",
|
||||
".lua",
|
||||
".i3u",
|
||||
".lang",
|
||||
".lock",
|
||||
".log",
|
||||
".markdown",
|
||||
".md",
|
||||
".nut",
|
||||
".php",
|
||||
".plist",
|
||||
".prefs",
|
||||
".py",
|
||||
".pyc",
|
||||
".qss",
|
||||
".sh",
|
||||
".toml",
|
||||
".ts",
|
||||
".mkd",
|
||||
".rmd",
|
||||
".txt",
|
||||
".vcfg",
|
||||
".vdf",
|
||||
".vmt",
|
||||
".vqlayout",
|
||||
".vsc",
|
||||
".vsnd_template",
|
||||
".xml",
|
||||
".yaml",
|
||||
".yml",
|
||||
"contributing",
|
||||
"license",
|
||||
"readme",
|
||||
}
|
||||
_PRESENTATION_SET: set[str] = {
|
||||
".key",
|
||||
@@ -310,9 +358,16 @@ class MediaCategories:
|
||||
".ppt",
|
||||
".pptx",
|
||||
}
|
||||
_PROGRAM_SET: set[str] = {".app", ".exe"}
|
||||
_SOURCE_ENGINE_SET: set[str] = {
|
||||
".vtf",
|
||||
_PROGRAM_SET: set[str] = {".app", ".bin", ".exe"}
|
||||
_SOURCE_ENGINE_SET: set[str] = {".vtf"}
|
||||
_SHADER_SET: set[str] = {
|
||||
".effect",
|
||||
".frag",
|
||||
".fsh",
|
||||
".glsl",
|
||||
".shader",
|
||||
".vert",
|
||||
".vsh",
|
||||
}
|
||||
_SHORTCUT_SET: set[str] = {".desktop", ".lnk", ".url"}
|
||||
_SPREADSHEET_SET: set[str] = {
|
||||
@@ -373,6 +428,12 @@ class MediaCategories:
|
||||
is_iana=False,
|
||||
name="blender",
|
||||
)
|
||||
CODE_TYPES = MediaCategory(
|
||||
media_type=MediaType.CODE,
|
||||
extensions=_CODE_SET,
|
||||
is_iana=False,
|
||||
name="code",
|
||||
)
|
||||
DATABASE_TYPES = MediaCategory(
|
||||
media_type=MediaType.DATABASE,
|
||||
extensions=_DATABASE_SET,
|
||||
@@ -439,6 +500,12 @@ class MediaCategories:
|
||||
is_iana=False,
|
||||
name="installer",
|
||||
)
|
||||
IWORK_TYPES = MediaCategory(
|
||||
media_type=MediaType.IWORK,
|
||||
extensions=_IWORK_SET,
|
||||
is_iana=False,
|
||||
name="iwork",
|
||||
)
|
||||
MATERIAL_TYPES = MediaCategory(
|
||||
media_type=MediaType.MATERIAL,
|
||||
extensions=_MATERIAL_SET,
|
||||
@@ -471,7 +538,7 @@ class MediaCategories:
|
||||
)
|
||||
PLAINTEXT_TYPES = MediaCategory(
|
||||
media_type=MediaType.PLAINTEXT,
|
||||
extensions=_PLAINTEXT_SET,
|
||||
extensions=_PLAINTEXT_SET | _CODE_SET,
|
||||
is_iana=False,
|
||||
name="plaintext",
|
||||
)
|
||||
@@ -487,6 +554,12 @@ class MediaCategories:
|
||||
is_iana=False,
|
||||
name="program",
|
||||
)
|
||||
SHADER_TYPES = MediaCategory(
|
||||
media_type=MediaType.SHADER,
|
||||
extensions=_SHADER_SET,
|
||||
is_iana=False,
|
||||
name="shader",
|
||||
)
|
||||
SHORTCUT_TYPES = MediaCategory(
|
||||
media_type=MediaType.SHORTCUT,
|
||||
extensions=_SHORTCUT_SET,
|
||||
@@ -535,6 +608,7 @@ class MediaCategories:
|
||||
IMAGE_TYPES,
|
||||
IMAGE_VECTOR_TYPES,
|
||||
INSTALLER_TYPES,
|
||||
IWORK_TYPES,
|
||||
MATERIAL_TYPES,
|
||||
MODEL_TYPES,
|
||||
OPEN_DOCUMENT_TYPES,
|
||||
@@ -543,6 +617,8 @@ class MediaCategories:
|
||||
PLAINTEXT_TYPES,
|
||||
PRESENTATION_TYPES,
|
||||
PROGRAM_TYPES,
|
||||
CODE_TYPES,
|
||||
SHADER_TYPES,
|
||||
SHORTCUT_TYPES,
|
||||
SOURCE_ENGINE_TYPES,
|
||||
SPREADSHEET_TYPES,
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
"path": "qt/images/file_icons/affinity_photo.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"archive": {
|
||||
"path": "qt/images/file_icons/archive.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"audio": {
|
||||
"path": "qt/images/file_icons/audio.png",
|
||||
"mode": "pil"
|
||||
@@ -51,10 +55,18 @@
|
||||
"path": "qt/images/file_icons/blender.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"database": {
|
||||
"path": "qt/images/file_icons/database.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"document": {
|
||||
"path": "qt/images/file_icons/document.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"ebook": {
|
||||
"path": "qt/images/file_icons/ebook.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"file_generic": {
|
||||
"path": "qt/images/file_icons/file_generic.png",
|
||||
"mode": "pil"
|
||||
@@ -87,6 +99,14 @@
|
||||
"path": "qt/images/file_icons/program.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shader": {
|
||||
"path": "qt/images/file_icons/shader.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"shortcut": {
|
||||
"path": "qt/images/file_icons/shortcut.png",
|
||||
"mode": "pil"
|
||||
},
|
||||
"spreadsheet": {
|
||||
"path": "qt/images/file_icons/spreadsheet.png",
|
||||
"mode": "pil"
|
||||
|
||||
@@ -12,6 +12,7 @@ import cv2
|
||||
import rawpy
|
||||
import structlog
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from PIL.Image import DecompressionBombError
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QSize, Qt
|
||||
from PySide6.QtGui import QAction, QMovie, QResizeEvent
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QStackedLayout, QWidget
|
||||
@@ -32,6 +33,7 @@ if typing.TYPE_CHECKING:
|
||||
from tagstudio.qt.ts_qt import QtDriver
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
|
||||
|
||||
class PreviewThumb(QWidget):
|
||||
@@ -271,7 +273,12 @@ class PreviewThumb(QWidget):
|
||||
image = Image.open(str(filepath))
|
||||
stats["width"] = image.width
|
||||
stats["height"] = image.height
|
||||
except (UnidentifiedImageError, FileNotFoundError, NotImplementedError) as e:
|
||||
except (
|
||||
DecompressionBombError,
|
||||
FileNotFoundError,
|
||||
NotImplementedError,
|
||||
UnidentifiedImageError,
|
||||
) as e:
|
||||
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
|
||||
@@ -331,7 +338,7 @@ class PreviewThumb(QWidget):
|
||||
movie.start()
|
||||
|
||||
stats["duration"] = movie.frameCount() // 60
|
||||
except UnidentifiedImageError as e:
|
||||
except (UnidentifiedImageError, FileNotFoundError) as e:
|
||||
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
|
||||
return self._display_fallback_image(filepath, ext)
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ import zipfile
|
||||
from copy import deepcopy
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from warnings import catch_warnings
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import rawpy
|
||||
import structlog
|
||||
from cv2.typing import MatLike
|
||||
from mutagen import MutagenError, flac, id3, mp4
|
||||
from PIL import (
|
||||
Image,
|
||||
@@ -71,11 +73,12 @@ from tagstudio.qt.resource_manager import ResourceManager
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
Image.MAX_IMAGE_PIXELS = None
|
||||
register_heif_opener()
|
||||
register_avif_opener()
|
||||
|
||||
try:
|
||||
import pillow_jxl # noqa: F401
|
||||
import pillow_jxl # noqa: F401 # pyright: ignore[reportUnusedImport]
|
||||
except ImportError:
|
||||
logger.exception('[ThumbRenderer] Could not import the "pillow_jxl" module')
|
||||
|
||||
@@ -102,11 +105,11 @@ class ThumbRenderer(QObject):
|
||||
# Cached thumbnail elements.
|
||||
# Key: Size + Pixel Ratio Tuple + Radius Scale
|
||||
# (Ex. (512, 512, 1.25, 4))
|
||||
self.thumb_masks: dict = {}
|
||||
self.raised_edges: dict = {}
|
||||
self.thumb_masks: dict[tuple[int, int, float, float], Image.Image] = {}
|
||||
self.raised_edges: dict[tuple[int, int, float], tuple[Image.Image, Image.Image]] = {}
|
||||
|
||||
# Key: ("name", UiColor, 512, 512, 1.25)
|
||||
self.icons: dict = {}
|
||||
self.icons: dict[tuple[str, UiColor, int, int, float], Image.Image] = {}
|
||||
|
||||
def _get_resource_id(self, url: Path) -> str:
|
||||
"""Return the name of the icon resource to use for a file type.
|
||||
@@ -119,6 +122,14 @@ class ThumbRenderer(QObject):
|
||||
ext = url.suffix.lower()
|
||||
types: set[MediaType] = MediaCategories.get_types(ext, mime_fallback=True)
|
||||
|
||||
# Manual icon overrides.
|
||||
if ext in {".gif", ".vtf"}:
|
||||
return MediaType.IMAGE
|
||||
elif ext in {".dll", ".pyc", ".o", ".dylib"}:
|
||||
return MediaType.PROGRAM
|
||||
elif ext in {".mscz"}: # noqa: SIM114
|
||||
return MediaType.TEXT
|
||||
|
||||
# Loop though the specific (non-IANA) categories and return the string
|
||||
# name of the first matching category found.
|
||||
for cat in MediaCategories.ALL_CATEGORIES:
|
||||
@@ -149,7 +160,7 @@ class ThumbRenderer(QObject):
|
||||
if scale_radius:
|
||||
radius_scale = max(size[0], size[1]) / thumb_scale
|
||||
|
||||
item: Image.Image = self.thumb_masks.get((*size, pixel_ratio, radius_scale))
|
||||
item: Image.Image | None = self.thumb_masks.get((*size, pixel_ratio, radius_scale))
|
||||
if not item:
|
||||
item = self._render_mask(size, pixel_ratio, radius_scale)
|
||||
self.thumb_masks[(*size, pixel_ratio, radius_scale)] = item
|
||||
@@ -166,7 +177,7 @@ class ThumbRenderer(QObject):
|
||||
size (tuple[int, int]): The size of the graphic.
|
||||
pixel_ratio (float): The screen pixel ratio.
|
||||
"""
|
||||
item: tuple[Image.Image, Image.Image] = self.raised_edges.get((*size, pixel_ratio))
|
||||
item: tuple[Image.Image, Image.Image] | None = self.raised_edges.get((*size, pixel_ratio))
|
||||
if not item:
|
||||
item = self._render_edge(size, pixel_ratio)
|
||||
self.raised_edges[(*size, pixel_ratio)] = item
|
||||
@@ -187,7 +198,7 @@ class ThumbRenderer(QObject):
|
||||
if name == "thumb_loading":
|
||||
draw_border = False
|
||||
|
||||
item: Image.Image = self.icons.get((name, color, *size, pixel_ratio))
|
||||
item: Image.Image | None = self.icons.get((name, color, *size, pixel_ratio))
|
||||
if not item:
|
||||
item_flat: Image.Image = self._render_icon(name, color, size, pixel_ratio, draw_border)
|
||||
edge: tuple[Image.Image, Image.Image] = self._get_edge(size, pixel_ratio)
|
||||
@@ -455,14 +466,15 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return im
|
||||
|
||||
def _audio_album_thumb(self, filepath: Path, ext: str) -> Image.Image | None:
|
||||
@staticmethod
|
||||
def _audio_album_thumb(filepath: Path, ext: str) -> Image.Image | None:
|
||||
"""Return an album cover thumb from an audio file if a cover is present.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
ext (str): The file extension (with leading ".").
|
||||
"""
|
||||
image: Image.Image = None
|
||||
image: Image.Image | None = None
|
||||
try:
|
||||
if not filepath.is_file():
|
||||
raise FileNotFoundError
|
||||
@@ -494,8 +506,9 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't read album artwork", path=filepath, error=type(e).__name__)
|
||||
return image
|
||||
|
||||
@staticmethod
|
||||
def _audio_waveform_thumb(
|
||||
self, filepath: Path, ext: str, size: int, pixel_ratio: float
|
||||
filepath: Path, ext: str, size: int, pixel_ratio: float
|
||||
) -> Image.Image | None:
|
||||
"""Render a waveform image from an audio file.
|
||||
|
||||
@@ -531,8 +544,9 @@ class ThumbRenderer(QObject):
|
||||
d = data[math.ceil(data_indices[i]) - 1]
|
||||
if count < samples_per_bar:
|
||||
count = count + 1
|
||||
if abs(d) > maximum_item:
|
||||
maximum_item = abs(d)
|
||||
with catch_warnings(record=True):
|
||||
if abs(d) > maximum_item:
|
||||
maximum_item = abs(d)
|
||||
else:
|
||||
max_array.append(maximum_item)
|
||||
|
||||
@@ -580,7 +594,8 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return im
|
||||
|
||||
def _blender(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _blender(filepath: Path) -> Image.Image:
|
||||
"""Get an emended thumbnail from a Blender file, if a thumbnail is present.
|
||||
|
||||
Args:
|
||||
@@ -614,7 +629,8 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _source_engine(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _source_engine(filepath: Path) -> Image.Image:
|
||||
"""This is a function to convert the VTF (Valve Texture Format) files to thumbnails.
|
||||
|
||||
It works using the VTF2IMG library for PILLOW.
|
||||
@@ -637,8 +653,8 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def _open_doc_thumb(cls, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _open_doc_thumb(filepath: Path) -> Image.Image:
|
||||
"""Extract and render a thumbnail for an OpenDocument file.
|
||||
|
||||
Args:
|
||||
@@ -660,8 +676,34 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def _epub_cover(cls, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _powerpoint_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Extract and render a thumbnail for a Microsoft PowerPoint file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
file_path_within_zip = "docProps/thumbnail.jpeg"
|
||||
im: Image.Image | None = None
|
||||
try:
|
||||
with zipfile.ZipFile(filepath, "r") as zip_file:
|
||||
# Check if the file exists in the zip
|
||||
if file_path_within_zip in zip_file.namelist():
|
||||
# Read the specific file into memory
|
||||
file_data = zip_file.read(file_path_within_zip)
|
||||
thumb_im = Image.open(BytesIO(file_data))
|
||||
if thumb_im:
|
||||
im = Image.new("RGB", thumb_im.size, color="#1e1e1e")
|
||||
im.paste(thumb_im)
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath)
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _epub_cover(filepath: Path) -> Image.Image:
|
||||
"""Extracts and returns the first image found in the ePub file at the given filepath.
|
||||
|
||||
Args:
|
||||
@@ -739,12 +781,13 @@ class ThumbRenderer(QObject):
|
||||
cropped_im,
|
||||
box=(margin, margin + ((size - new_y) // 2)),
|
||||
)
|
||||
im = self._apply_overlay_color(bg, UiColor.PURPLE)
|
||||
im = self._apply_overlay_color(bg, UiColor.BLUE)
|
||||
except OSError as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _font_long_thumb(self, filepath: Path, size: int) -> Image.Image:
|
||||
@staticmethod
|
||||
def _font_long_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
"""Render a large font preview ("Alphabet") thumbnail from a font file.
|
||||
|
||||
Args:
|
||||
@@ -775,7 +818,8 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _image_raw_thumb(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _image_raw_thumb(filepath: Path) -> Image.Image:
|
||||
"""Render a thumbnail for a RAW image type.
|
||||
|
||||
Args:
|
||||
@@ -799,7 +843,8 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _image_thumb(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _image_thumb(filepath: Path) -> Image.Image:
|
||||
"""Render a thumbnail for a standard image type.
|
||||
|
||||
Args:
|
||||
@@ -823,8 +868,8 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def _image_vector_thumb(cls, filepath: Path, size: int) -> Image.Image:
|
||||
@staticmethod
|
||||
def _image_vector_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
"""Render a thumbnail for a vector image, such as SVG.
|
||||
|
||||
Args:
|
||||
@@ -860,7 +905,46 @@ class ThumbRenderer(QObject):
|
||||
buffer.close()
|
||||
return im
|
||||
|
||||
def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image:
|
||||
@staticmethod
|
||||
def _iwork_thumb(filepath: Path) -> Image.Image:
|
||||
"""Extract and render a thumbnail for an Apple iWork (Pages, Numbers, Keynote) file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
preview_thumb_dir = "preview.jpg"
|
||||
quicklook_thumb_dir = "QuickLook/Thumbnail.jpg"
|
||||
im: Image.Image | None = None
|
||||
|
||||
def get_image(path: str) -> Image.Image | None:
|
||||
thumb_im: Image.Image | None = None
|
||||
# Read the specific file into memory
|
||||
file_data = zip_file.read(path)
|
||||
thumb_im = Image.open(BytesIO(file_data))
|
||||
return thumb_im
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(filepath, "r") as zip_file:
|
||||
thumb: Image.Image | None = None
|
||||
|
||||
# Check if the file exists in the zip
|
||||
if preview_thumb_dir in zip_file.namelist():
|
||||
thumb = get_image(preview_thumb_dir)
|
||||
elif quicklook_thumb_dir in zip_file.namelist():
|
||||
thumb = get_image(quicklook_thumb_dir)
|
||||
else:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath)
|
||||
|
||||
if thumb:
|
||||
im = Image.new("RGB", thumb.size, color="#1e1e1e")
|
||||
im.paste(thumb)
|
||||
except zipfile.BadZipFile as e:
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
|
||||
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _model_stl_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
"""Render a thumbnail for an STL file.
|
||||
|
||||
Args:
|
||||
@@ -892,8 +976,8 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return im
|
||||
|
||||
@classmethod
|
||||
def _pdf_thumb(cls, filepath: Path, size: int) -> Image.Image:
|
||||
@staticmethod
|
||||
def _pdf_thumb(filepath: Path, size: int) -> Image.Image:
|
||||
"""Render a thumbnail for a PDF file.
|
||||
|
||||
filepath (Path): The path of the file.
|
||||
@@ -933,20 +1017,21 @@ class ThumbRenderer(QObject):
|
||||
buffer: QBuffer = QBuffer()
|
||||
buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
try:
|
||||
q_image.save(buffer, "PNG") # type: ignore[call-overload]
|
||||
q_image.save(buffer, "PNG") # type: ignore # pyright: ignore
|
||||
im = Image.open(BytesIO(buffer.buffer().data()))
|
||||
finally:
|
||||
buffer.close()
|
||||
# Replace transparent pixels with white (otherwise Background defaults to transparent)
|
||||
return replace_transparent_pixels(im)
|
||||
|
||||
def _text_thumb(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _text_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Render a thumbnail for a plaintext file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image = None
|
||||
im: Image.Image | None = None
|
||||
|
||||
bg_color: str = (
|
||||
"#1e1e1e"
|
||||
@@ -977,13 +1062,15 @@ class ThumbRenderer(QObject):
|
||||
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
|
||||
return im
|
||||
|
||||
def _video_thumb(self, filepath: Path) -> Image.Image:
|
||||
@staticmethod
|
||||
def _video_thumb(filepath: Path) -> Image.Image | None:
|
||||
"""Render a thumbnail for a video file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path of the file.
|
||||
"""
|
||||
im: Image.Image = None
|
||||
im: Image.Image | None = None
|
||||
frame: MatLike | None = None
|
||||
try:
|
||||
if is_readable_video(filepath):
|
||||
video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG)
|
||||
@@ -1007,8 +1094,9 @@ class ThumbRenderer(QObject):
|
||||
video.set(cv2.CAP_PROP_POS_FRAMES, i)
|
||||
else:
|
||||
break
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
im = Image.fromarray(frame)
|
||||
if frame:
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
im = Image.fromarray(frame)
|
||||
except (
|
||||
UnidentifiedImageError,
|
||||
cv2.error,
|
||||
@@ -1073,7 +1161,7 @@ class ThumbRenderer(QObject):
|
||||
cached_path: Path | None = None
|
||||
|
||||
if hash_value and self.lib.library_dir:
|
||||
cached_path = (
|
||||
cached_path = Path(
|
||||
self.lib.library_dir
|
||||
/ TS_FOLDER_NAME
|
||||
/ THUMB_CACHE_NAME
|
||||
@@ -1084,7 +1172,7 @@ class ThumbRenderer(QObject):
|
||||
try:
|
||||
image = Image.open(cached_path)
|
||||
if not image:
|
||||
raise UnidentifiedImageError
|
||||
raise UnidentifiedImageError # pyright: ignore[reportUnreachable]
|
||||
ThumbRenderer.last_cache_folder = folder
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -1106,7 +1194,7 @@ class ThumbRenderer(QObject):
|
||||
|
||||
image: Image.Image | None = None
|
||||
# Try to get a non-loading thumbnail for the grid.
|
||||
if not is_loading and is_grid_thumb and filepath and filepath != ".":
|
||||
if not is_loading and is_grid_thumb and filepath and filepath != Path("."):
|
||||
# Attempt to retrieve cached image from disk
|
||||
mod_time: str = ""
|
||||
with contextlib.suppress(Exception):
|
||||
@@ -1244,7 +1332,7 @@ class ThumbRenderer(QObject):
|
||||
|
||||
"""
|
||||
adj_size = math.ceil(max(base_size[0], base_size[1]) * pixel_ratio)
|
||||
image: Image.Image = None
|
||||
image: Image.Image | None = None
|
||||
_filepath: Path = Path(filepath)
|
||||
savable_media_type: bool = True
|
||||
|
||||
@@ -1253,7 +1341,7 @@ class ThumbRenderer(QObject):
|
||||
# Missing Files ================================================
|
||||
if not _filepath.exists():
|
||||
raise FileNotFoundError
|
||||
ext: str = _filepath.suffix.lower()
|
||||
ext: str = _filepath.suffix.lower() if _filepath.suffix else _filepath.stem.lower()
|
||||
# Images =======================================================
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.IMAGE_TYPES, mime_fallback=True
|
||||
@@ -1276,11 +1364,17 @@ class ThumbRenderer(QObject):
|
||||
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._video_thumb(_filepath)
|
||||
# PowerPoint Slideshow
|
||||
elif ext in {".pptx"}:
|
||||
image = self._powerpoint_thumb(_filepath)
|
||||
# OpenDocument/OpenOffice ======================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._open_doc_thumb(_filepath)
|
||||
# Apple iWork Suite ============================================
|
||||
elif MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES):
|
||||
image = self._iwork_thumb(_filepath)
|
||||
# Plain Text ===================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True
|
||||
@@ -1351,7 +1445,7 @@ class ThumbRenderer(QObject):
|
||||
|
||||
return image
|
||||
|
||||
def _resize_image(self, image, size: tuple[int, int]) -> Image.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
|
||||
|
||||
|
||||
BIN
src/tagstudio/resources/qt/images/file_icons/archive.png
Normal file
BIN
src/tagstudio/resources/qt/images/file_icons/archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src/tagstudio/resources/qt/images/file_icons/database.png
Normal file
BIN
src/tagstudio/resources/qt/images/file_icons/database.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/tagstudio/resources/qt/images/file_icons/ebook.png
Normal file
BIN
src/tagstudio/resources/qt/images/file_icons/ebook.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src/tagstudio/resources/qt/images/file_icons/shader.png
Normal file
BIN
src/tagstudio/resources/qt/images/file_icons/shader.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
src/tagstudio/resources/qt/images/file_icons/shortcut.png
Normal file
BIN
src/tagstudio/resources/qt/images/file_icons/shortcut.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Reference in New Issue
Block a user