From 8c4aa38eb120f9ff5d797a084e9c3cceabd1be0f Mon Sep 17 00:00:00 2001 From: Sola-ris <190788035+Sola-ris@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:43:30 +0200 Subject: [PATCH 1/3] feat: render .pxd thumbnails. --- docs/preview-support.md | 3 ++- src/tagstudio/core/media_types.py | 20 ++++++++++---------- src/tagstudio/qt/previews/renderer.py | 23 +++++++++++++---------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/preview-support.md b/docs/preview-support.md index 7565fbc21..dbda26220 100644 --- a/docs/preview-support.md +++ b/docs/preview-support.md @@ -83,7 +83,7 @@ Audio thumbnails will default to embedded cover art (if any) and fallback to gen Preview support for office documents or well-known project file formats varies by the format and whether or not embedded thumbnails are available to be read from. OpenDocument-based files are typically supported. | Filetype | Extensions | Preview Type | -| ------------------------------------ | --------------------- | -------------------------------------------------------------------------- | +|------------------------------------- |-----------------------|----------------------------------------------------------------------------| | Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } | | Clip Studio Paint | `.clip` | Embedded thumbnail | | Keynote (Apple iWork) | `.key` | Embedded thumbnail | @@ -98,6 +98,7 @@ Preview support for office documents or well-known project file formats varies b | Paint.NET | `.pdn` | Embedded thumbnail | | PDF | `.pdf` | First page render | | Photoshop | `.psd` | Flattened image render | +| Pixelmator Pro (Apple iWork) | `.pxd` | Embedded thumbnail | | PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } | ### :material-archive: Archives diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index db62d7807..0639401b4 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -12,14 +12,14 @@ logger = structlog.get_logger(__name__) 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"]), + {"aif", "aiff", "aifc"}, + {"html", "htm", "xhtml", "shtml", "dhtml"}, + {"jfif", "jpeg_large", "jpeg", "jpg_large", "jpg"}, + {"json", "jsonc", "json5"}, + {"md", "markdown", "mkd", "rmd"}, + {"tar.gz", "tgz"}, + {"xml", "xul"}, + {"yaml", "yml"}, ] @@ -75,7 +75,7 @@ class MediaCategory: extensions (set[str]): The set of file extensions associated with this category. Includes leading ".", all lowercase, and does not need to be unique to this category. - is_iana (bool): Represents whether or not this is an IANA registered category. + is_iana (bool): Represents whether this is an IANA registered category. """ media_type: MediaType @@ -336,7 +336,7 @@ class MediaCategories: ".webp", } _INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"} - _IWORK_SET: set[str] = {".key", ".pages", ".numbers"} + _IWORK_SET: set[str] = {".key", ".numbers", ".pages", ".pxd"} _MATERIAL_SET: set[str] = {".mtl"} _MDIPACK_SET: set[str] = {".mdp"} _MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"} diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index f3471e5f1..ffd5729bb 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -623,7 +623,7 @@ def _apply_edge( image (Image.Image): The image to apply the edge to. edge (tuple[Image.Image, Image.Image]): The edge images to apply. Item 0 is the inner highlight, and item 1 is the outer shadow. - faded (bool): Whether or not to apply a faded version of the edge. + faded (bool): Whether to apply a faded version of the edge. Used for light themes. """ opacity: float = 1.0 if not faded else 0.8 @@ -847,7 +847,7 @@ def _open_doc_thumb(filepath: Path) -> Image.Image | None: @staticmethod def _krita_thumb(filepath: Path) -> Image.Image | None: - """Extract and render a thumbnail for an Krita file. + """Extract and render a thumbnail for a Krita file. Args: filepath (Path): The path of the file. @@ -1219,13 +1219,17 @@ def _image_vector_thumb(filepath: Path, size: int) -> Image.Image: @staticmethod def _iwork_thumb(filepath: Path) -> Image.Image | None: - """Extract and render a thumbnail for an Apple iWork (Pages, Numbers, Keynote) file. + """Render a thumbnail for an Apple iWork (Pages, Numbers, Keynote, Pixelmator) file. Args: filepath (Path): The path of the file. """ - preview_thumb_dir = "preview.jpg" - quicklook_thumb_dir = "QuickLook/Thumbnail.jpg" + thumb_files: list[str] = [ + "preview.jpg", + "QuickLook/Preview.heic", + "QuickLook/Thumbnail.jpg", + "QuickLook/Thumbnail.heic", + ] im: Image.Image | None = None def get_image(path: str) -> Image.Image | None: @@ -1240,10 +1244,9 @@ def get_image(path: str) -> Image.Image | None: 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) + for thumb_file in thumb_files: + if thumb_file in zip_file.namelist(): + thumb = get_image(thumb_file) else: logger.error("Couldn't render thumbnail", filepath=filepath) @@ -1317,7 +1320,7 @@ def _pdf_thumb(filepath: Path, size: int) -> Image.Image | None: # Enlarge image for antialiasing scale_factor = 2.5 page_size *= scale_factor - # Render image with no anti-aliasing for speed + # Render image with no antialiasing for speed render_options: QPdfDocumentRenderOptions = QPdfDocumentRenderOptions() render_options.setRenderFlags( QPdfDocumentRenderOptions.RenderFlag.TextAliased From b2a7c5c56e51e09195fe8f3b8694820efda41379 Mon Sep 17 00:00:00 2001 From: Sola-ris <190788035+Sola-ris@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:36:51 +0200 Subject: [PATCH 2/3] move .pxd to _DOCUMENT_SET, rename method, fix doc and typos. --- src/tagstudio/core/media_types.py | 3 ++- src/tagstudio/qt/previews/renderer.py | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index 0639401b4..12f6ae8aa 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -257,6 +257,7 @@ class MediaCategories: ".odt", ".pages", ".pdf", + ".pxd", ".rtf", ".tex", ".wpd", @@ -336,7 +337,7 @@ class MediaCategories: ".webp", } _INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"} - _IWORK_SET: set[str] = {".key", ".numbers", ".pages", ".pxd"} + _IWORK_SET: set[str] = {".key", ".numbers", ".pages"} _MATERIAL_SET: set[str] = {".mtl"} _MDIPACK_SET: set[str] = {".mdp"} _MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"} diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index ffd5729bb..6362bd300 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -1218,8 +1218,8 @@ def _image_vector_thumb(filepath: Path, size: int) -> Image.Image: return im @staticmethod - def _iwork_thumb(filepath: Path) -> Image.Image | None: - """Render a thumbnail for an Apple iWork (Pages, Numbers, Keynote, Pixelmator) file. + def _apple_embedded_thumb(filepath: Path) -> Image.Image | None: + """Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio). Args: filepath (Path): The path of the file. @@ -1317,10 +1317,10 @@ def _pdf_thumb(filepath: Path, size: int) -> Image.Image | None: page_size *= size / page_size.height() else: page_size *= size / page_size.width() - # Enlarge image for antialiasing + # Enlarge image for anti-aliasing scale_factor = 2.5 page_size *= scale_factor - # Render image with no antialiasing for speed + # Render image with no anti-aliasing for speed render_options: QPdfDocumentRenderOptions = QPdfDocumentRenderOptions() render_options.setRenderFlags( QPdfDocumentRenderOptions.RenderFlag.TextAliased @@ -1868,8 +1868,11 @@ def _render( ): image = self._open_doc_thumb(_filepath) # Apple iWork Suite ============================================ - elif MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES): - image = self._iwork_thumb(_filepath) + elif ( + MediaCategories.is_ext_in_category(ext, MediaCategories.IWORK_TYPES) + or ext == ".pxd" + ): + image = self._apple_embedded_thumb(_filepath) # Plain Text =================================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True From c97f481933f6c16d76ca314410ff11cade35c4e1 Mon Sep 17 00:00:00 2001 From: Sola-ris <190788035+Sola-ris@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:37:22 +0200 Subject: [PATCH 3/3] add missing break. --- src/tagstudio/qt/previews/renderer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index 6362bd300..13ae74756 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -1247,6 +1247,7 @@ def get_image(path: str) -> Image.Image | None: for thumb_file in thumb_files: if thumb_file in zip_file.namelist(): thumb = get_image(thumb_file) + break else: logger.error("Couldn't render thumbnail", filepath=filepath)