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 00000000..d63eb709 Binary files /dev/null and b/src/tagstudio/resources/qt/images/file_icons/archive.png differ diff --git a/src/tagstudio/resources/qt/images/file_icons/database.png b/src/tagstudio/resources/qt/images/file_icons/database.png new file mode 100644 index 00000000..d1636ec4 Binary files /dev/null and b/src/tagstudio/resources/qt/images/file_icons/database.png differ 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 00000000..9b80839a Binary files /dev/null and b/src/tagstudio/resources/qt/images/file_icons/ebook.png differ 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 00000000..0c0c10af Binary files /dev/null and b/src/tagstudio/resources/qt/images/file_icons/shader.png differ 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 00000000..12f8b409 Binary files /dev/null and b/src/tagstudio/resources/qt/images/file_icons/shortcut.png differ