From d7d7e21d13126f1272a2a3e552b4f3ee7054f324 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Sun, 30 Mar 2025 19:24:10 -0700 Subject: [PATCH] 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 --- src/tagstudio/core/media_types.py | 204 ++++++++++++------ src/tagstudio/qt/resources.json | 20 ++ .../qt/widgets/preview/preview_thumb.py | 11 +- src/tagstudio/qt/widgets/thumb_renderer.py | 174 +++++++++++---- .../qt/images/file_icons/archive.png | Bin 0 -> 8592 bytes .../qt/images/file_icons/database.png | Bin 0 -> 15713 bytes .../resources/qt/images/file_icons/ebook.png | Bin 0 -> 6173 bytes .../resources/qt/images/file_icons/shader.png | Bin 0 -> 16977 bytes .../qt/images/file_icons/shortcut.png | Bin 0 -> 8741 bytes 9 files changed, 303 insertions(+), 106 deletions(-) create mode 100644 src/tagstudio/resources/qt/images/file_icons/archive.png create mode 100644 src/tagstudio/resources/qt/images/file_icons/database.png create mode 100644 src/tagstudio/resources/qt/images/file_icons/ebook.png create mode 100644 src/tagstudio/resources/qt/images/file_icons/shader.png create mode 100644 src/tagstudio/resources/qt/images/file_icons/shortcut.png diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index f784be86..f13ce2a0 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -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, diff --git a/src/tagstudio/qt/resources.json b/src/tagstudio/qt/resources.json index 8966e89f..216b9a2b 100644 --- a/src/tagstudio/qt/resources.json +++ b/src/tagstudio/qt/resources.json @@ -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" diff --git a/src/tagstudio/qt/widgets/preview/preview_thumb.py b/src/tagstudio/qt/widgets/preview/preview_thumb.py index a94d918d..7440da82 100644 --- a/src/tagstudio/qt/widgets/preview/preview_thumb.py +++ b/src/tagstudio/qt/widgets/preview/preview_thumb.py @@ -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) diff --git a/src/tagstudio/qt/widgets/thumb_renderer.py b/src/tagstudio/qt/widgets/thumb_renderer.py index c7014dfd..4b0892bd 100644 --- a/src/tagstudio/qt/widgets/thumb_renderer.py +++ b/src/tagstudio/qt/widgets/thumb_renderer.py @@ -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 diff --git a/src/tagstudio/resources/qt/images/file_icons/archive.png b/src/tagstudio/resources/qt/images/file_icons/archive.png new file mode 100644 index 0000000000000000000000000000000000000000..d63eb7099e2be90b0c1ff12983414779c5073ec3 GIT binary patch literal 8592 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 arp!BI2jR-X$z|TFz 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

xBQB;gLxU2NJ?3rjb$c5-I z>2<%WwkGXh24HT}Fu3kxu}UWa8vCrgB-DqIP(8ay-u|#5%*3xu41gCNUd(97w$)r> zC%p%kl;-&wOqcS!MCYE~F?HC^WH=spIq!Q>6i5pD!2Q}2vt>W?o)QYzBz@-RMyq2Qpfe>c)@!yoxrc=R*KYs}T}l#$-l zre))|@o8X3uo0gX!A#(2moz8>id8EK!wYRM+iPD+d|2+9rxAByT{5I_Q%b3jhKN6E zwAxi|-@jaZLrhfCNAQ`{7G?r+DXh8vK_nT9>ziVfCoL%C3rA(4{ zuIv2>@&~b0$oDi)#+Vw#*Vz<{`6Lni{&um8=1HkfEnNK`N^L6%Uv-uF);i4n9t*K( z8~2tRtOFOree#>lyw_da$aQ+Tin%}S0fGTi;iw=j36=cU^ROM{R={tJW2%JY@KbHw zt#kHK4&SfKF~>o*0OJl{P2lM;S7+|iTQ!+p!G}tHr#w1mE2%9i?vaf7uh-Dy)N1HY zg&_dI5LrX?ay8r~Yrk7m*h!H5FpiU0*aFzbkS|4n6!epiu~Rlj=uNz-#TTW$*-DbM=lp1Te7Ruiivl-Lw-5SOn zZ8-^GioHPTevkA^xtj*DxhOtavRC^lCx`wwUg?aJ#0?C2lMzdnwY(u5X>wKNhI!`WQXHMxiBHaC|W*PCeLXdu9SK zJ>D*PN@3!WJ*U~y0d837_F{Sh1bMg7B=K)T6zG=1EWmPg0F@>6pu6w7~mbNSeGGf&{xk)Z>j+)gO zX^zhW;ui5-?8yVmH)9Np2fq3vWN2jIv4`~`bqK4nWrrqS)p-FGXZi;QxzG1u zbysgTIGigsOWc#0j*F(H#6vWB-jObRv*F0ry0BxC|E27se=_ipi>iLd`ag}2l}tTY z$nK+DcJ3Pw84PMvh%nf19}iI^`;+J7JB&L1kqGz9d2g`f-wgQvD+kF>r@?GH%#bB% z%^?Wq44t+inG%eymp|Cjo9(dUiu3&2oH2Hv1re=4t>M3>9(|uTh?A>kQN7hf_tm_N zH>sX1v8|7oK8=MJ=C}y~9eauqskz;grbL`RODTlv%eb3B!K{6tQHd%pS70j(+UM*COOyNxTkd|xS zu%3Q@$;mTLLgtYj@dU6E#MX|#A(Fo|>l?vkxV7b`N_v1=;4bdjt>LznCO=0X-e<1l z$J^0rsR-9&)oN&^HIi@9%2d_!4zs^iMwun^#oJ69=;Zo!E?s*Dr)(s`PfaxPZZcg_ zUyskNTDCxIVgu z(zhFwh{yB=!lys{n!XL&cQhHl5v?M3brIKgJxZ`9;d!Ozrk#G9Q&N(UN`Qj4}t5t>81gJYJd{@_YkzP_<%CcFiYhLIX}l+R((gon{V2NocyX>iL(|o}x(qOIQa3gi zseq=K>zOK}#837ZaJNn;Ble&I01tVG=i`jztL$Ly|}a>%U@ zAGGx`KKV0=%F1=gpm72J9AWNm;p3h2hUU?EhEFO+ezf>d6Z@pacP)3yh?Yg?_3Uyc z3IHIX2LJ+Df7qeQ^>2pew-1SD`uF;GgZvq_O~b=`5s&VVdy|3kp78)z6}&&k0+9IY zue44ob9T@Ht-rC2WHHjc41NYo`Gc%m zEk%X2I`=@@P9R^P#8J2J*UIdhl0Cl$17Poj0YKu0J%(eed7pvGz)JQvNQ-sk1K`7U zonauk38wmVjQ%u{t(DDIFSf-8_6`nk1eKW}O7n!K262#`9&U4MD&+$7!~it^VT-)P zH(Zj`Z9puqJ&sfEY3l_VoIww(nuD7n)m9saGEkvCLu9u0M=t}=($EDe$e<<&(_%ck zI0Q>8#$=m3!KB`r(gFej8Z+>yiGB0FjDP|rCsBpX&nzt33nnbQPxCF*p1BnkAdo)D zXF*V&jruvJjr{EQCvtwj$Uwa;P#>lNzr;E0QcG`6Ol#;47ho43ah!ZL)wf^eNMfS4eC?wP-#v`VDeCeQ|*T$G$Pf@b=szZ5&pwSIgeH56Ukdr4a2&t~-vt|Ci_aLYn2+_KMlXx| z_l(U&xLxkH)eK>#!V}Yhl(X2ErsmQ7hOHZg!W{xWhU1#|L@NrO-Ubi{p@v44BU@!p zSEda1g>`XR>~o`v@lE|1?a$xq3yE<>iYJC3ZJJ4%x7@QeS(fNrm;~FRx5S#UOeN23 zjm_ma(S5yRVuE7+VwXNOIl?d_j(V-(LZTf4$Heyh?5ptRI~>3ULg4WwIp+s89fyfg zUNTF8(``8uj_KTmLfc07qynfSfR-$li}BH1@#z=EmmOQLMg({?GK+QFb#LYz?MH&Q z9Az_wP0^b?D@iDE9Ep$Ub1-P7uN#YRAg^@)oK}4G=K_DJWgd0GOJFIOB2wh?R9Z!# z>}WJXB4nDy{_UfWLkK%IS{pgQ)DRAPG%Bi2$ZSal#L8K{cwukfQnokY8==3V|0;bS z6+YcAnH(s-)v13vPJ=N|U(v1Zbi_olaxU2DdPOD`Mi%YvYq!fipDWFY6y1^lpyy-M zX}U>{gSiS^E~@`WMxYoj;j^`2IIW)EP+Ift?FHjaHoAE=}gd}(|pCp~gCeEket+SZ^{ z68=hU+QaAKBMg>b*`%NxuSJvAmX);$5avNmE6je%w*-%+$wki-4vnSzai2H`YuAV)7Bl*b|-@+IDT zs-M4znNa4k9VT4CcdAWUG*?QjTOBq3MoXf{Q)xAt8?rUht3|Lt^%}k$#T;+4`@{C$ zJ(9_M_MLR|OVH-_JgVG03ic1Sf??yqrfbjipCun|sLkr~3oZ(F0<_!i7#W&AHc^dK z4+(RCg+Tb=M5fEd_l98Ma!p5@IGTjcAPqqzzSi`^-`TT+UgpuGPx;7JO?84~{XNJQ zl7f{2Hewg}NJ+Uhp$X~YJg)n!@BpvBI+kWT7$z`(tl4<6{^Wsxxx%G< z&XB!#1%SQbM=*6i#9sbwh)p5AkVpLb_MX8MBKzd{F$Grx#*Mbydt=^UBPvr76qPxq zS&(l4fRmrGjH*nV*f5a+l(DG4#fr63S&PYiv3%_(7il2uA(8_*55f?CNr_hORslmsoxqb$_1abw?K@5}8X#v+V*d?<&%tEXKc z4ys>_PxC$xNFFUT?`BoVyC46-#UR&qRJ`vpf?`VH+B7LtZfN@SL5eLFRyE`u41;77 zhM_LaHnB2?Ojf>A(o`zl{|71ts^LaJeS;JHt3pN-XNV;gGB9+Y<<~W;+GYlca=;m^ z7XA^9q4}K)aT9TO8~R|}UtK7vHInJYY!#xT5$gMg068O!d5LDJoQiKatg%vBMlgdQ zhrV7Zx{k!fKp4r+?)WMX2{RQ=sI!IB7SXB!0EB_!OE-qU*y!3qzT-+{eu5~KhGP8E zGoA{s*l{B6_9hBe!F8g`Y(1-^>XgDY28Z1`L z^gCxRW{qLm1k5{QXP9hQ7JDwez;C?7_5nhu*QDr}6x1~<@9at0qGBL)C)hW2KvO`< zjMWwvrxT=7K^Xv_Gmh>tV3lIVk5Tf7T&*xD&T&NZAtk$j}Yh8R>OsE|x`U3`}FtP{f-s61Rl&fEPrtAgbXegoq2 zlrWd47XJs?qBKA51wZU-g*McFavl)-zHbNv8Z{A+exC!24NymD4HE=jQZ>5gk;5lb9-USV(^GVg8`ad#!cHogcH8-pBUbjg;!BEXJ1nUsCW(B7WD7zr%Hfqg|c1LOHH06O_ zZb?q?AsvMtgcg0?)%&W%pN~bYm$wy{bo?cG(MY_9I=;!3(gfyreJ?ZKBHpf+y`TJt zLEeD~cxHqkme=uadQKk_Q{TJ(hjZRZ)*7{=a^Cq(S%PovaVGpqa)F&Itw=0U*1r4( zX_V6hXzHv+8QOR9lAgYW-}QYT&mNx&?=$YzHxC{+U|fmI0#?UML8CJO<+cYbOg(cy z2Zl?{D-WNaho1DE!s=0sN#X$CdEIz@vv{Aw*(^xr`w=95eUu{ty%d%wAza_TOFBW~ zA#=9*@E{>$F&ShlVpG?-ZqOHH_05|SO`g`Y`E(MC|LO0mmCDv$(ABd6i_3Vrf} z-8fo*@ZL;7sDHMpy`U;gl}!ajEU)Hm%K5sAhG%{$yu=e3>m3ReEkd&AxXH0Q?`PZN#LYK_evi;TC=j%in55ApY>D&}whhb#8C|GZ)^;{)c@ zQgclE!%}31>%5clMOTEl!`TWqOQvn1hsj^6UCgSvYF6&OZscsrXHwt(>kbCf<^dDF zc85%NW*dgYPpGFN@%d~k17uB_@d*M*F&p8H&?B}LqR%t94)26ikubOI;nf!l`Gt=* ztOurQn3JQI)!GzAkdXJ!Ft^SkXIS9eG(R@v1 z-->6d7iglcZ%?k1#E;#v_GDVJbAE9v!tLtf7K39s;N4nkTj2eMtIrJ|8fN_8ziHY9 nDI{OmeCo_VBN2r$uiR(Ob0Mo^+1Pvm9t&agP4p^HIbZ%i2EoX* literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/file_icons/ebook.png b/src/tagstudio/resources/qt/images/file_icons/ebook.png new file mode 100644 index 0000000000000000000000000000000000000000..9b80839a5aab1869fa974607cda43051f4cedbe8 GIT binary patch literal 6173 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 literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/file_icons/shader.png b/src/tagstudio/resources/qt/images/file_icons/shader.png new file mode 100644 index 0000000000000000000000000000000000000000..0c0c10afd37a0f970cddb20a02cc4b9530788d94 GIT binary patch literal 16977 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 NOe~EbpY^)_{{X)gX8ZsE literal 0 HcmV?d00001 diff --git a/src/tagstudio/resources/qt/images/file_icons/shortcut.png b/src/tagstudio/resources/qt/images/file_icons/shortcut.png new file mode 100644 index 0000000000000000000000000000000000000000..12f8b409083b695789276c8e8aa705b2631d208b GIT binary patch literal 8741 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 C63xf} literal 0 HcmV?d00001