diff --git a/docs/library/index.md b/docs/library/index.md index 0b7d6718b..a15a6dd7f 100644 --- a/docs/library/index.md +++ b/docs/library/index.md @@ -97,6 +97,7 @@ Preview support for office documents or well-known project file formats varies b | OpenDocument Spreadsheet | `.ods`, `.fods` | Embedded thumbnail | | OpenDocument Text | `.odt`, `.fodt` | Embedded thumbnail | | Pages (Apple iWork) | `.pages` | Embedded thumbnail | +| Paint.NET | `.pdn` | Embedded thumbnail | | PDF | `.pdf` | First page render | | Photoshop | `.psd` | Flattened image render | | PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } | diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index 8659c389d..e7136500d 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -49,6 +49,7 @@ class MediaType(str, Enum): MODEL = "model" OPEN_DOCUMENT = "open_document" PACKAGE = "package" + PAINT_DOT_NET = "paint_dot_net" PDF = "pdf" PLAINTEXT = "plaintext" PRESENTATION = "presentation" @@ -358,6 +359,7 @@ class MediaCategories: ".pkg", ".xapk", } + _PAINT_DOT_NET_SET: set[str] = {".pdn"} _PDF_SET: set[str] = {".pdf"} _PLAINTEXT_SET: set[str] = { ".csv", @@ -554,6 +556,12 @@ class MediaCategories: is_iana=False, name="package", ) + PAINT_DOT_NET_TYPES = MediaCategory( + media_type=MediaType.PAINT_DOT_NET, + extensions=_PAINT_DOT_NET_SET, + is_iana=False, + name="paint.net", + ) PDF_TYPES = MediaCategory( media_type=MediaType.PDF, extensions=_PDF_SET, @@ -643,6 +651,7 @@ class MediaCategories: MODEL_TYPES, OPEN_DOCUMENT_TYPES, PACKAGE_TYPES, + PAINT_DOT_NET_TYPES, PDF_TYPES, PLAINTEXT_TYPES, PRESENTATION_TYPES, @@ -679,7 +688,7 @@ def is_ext_in_category(ext: str, media_cat: MediaCategory, mime_fallback: bool = Args: ext (str): File extension with a leading "." and in all lowercase. - media_cat (MediaCategory): The MediaCategory to to check for extension membership. + media_cat (MediaCategory): The MediaCategory to check for extension membership. mime_fallback (bool): Flag to guess MIME type if no set matches are made. """ return media_cat.contains(ext, mime_fallback) diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index f47d534ac..c07a438df 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -3,10 +3,12 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio +import base64 import contextlib import hashlib import math import os +import struct import tarfile import xml.etree.ElementTree as ET import zipfile @@ -1378,6 +1380,42 @@ def _video_thumb(filepath: Path) -> Image.Image | None: logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) return im + @staticmethod + def _pdn_thumb(filepath: Path) -> Image.Image | None: + """Extract the base64-encoded thumbnail from a .pdn file header. + + Args: + filepath (Path): The path of the .pdn file. + + Returns: + Image: the decoded PNG thumbnail or None by default. + """ + im: Image.Image | None = None + with open(filepath, "rb") as f: + try: + # First 4 bytes are the magic number + if f.read(4) != b"PDN3": + return im + + # Header length is a little-endian 24-bit int + header_size = struct.unpack("