feat: render .pdn thumbnails. (#1149)

* feat: render .pdn thumbnails.

* fix: add background to .pdn thumbnail

* doc: document .pdn support.

* fix: add PAINT_DOT_NET_TYPES to ALL_CATEGORIES.
This commit is contained in:
Sola-ris
2025-12-15 05:31:42 +01:00
committed by GitHub
parent 77274fb77f
commit 49e9ede387
3 changed files with 55 additions and 4 deletions

View File

@@ -89,6 +89,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" } |

View File

@@ -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 @@ class MediaCategories:
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)

View File

@@ -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
@@ -1377,6 +1379,42 @@ class ThumbRenderer(QObject):
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("<i", f.read(3) + b"\x00")[0]
thumb_element = ET.fromstring(f.read(header_size)).find("./*thumb")
if thumb_element is None:
return im
encoded_png = thumb_element.get("png")
if encoded_png:
decoded_png = base64.b64decode(encoded_png)
im = Image.open(BytesIO(decoded_png))
if im.mode == "RGBA":
new_bg = Image.new("RGB", im.size, color="#1e1e1e")
new_bg.paste(im, mask=im.getchannel(3))
im = new_bg
except Exception as e:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
return im
def render(
self,
timestamp: float,
@@ -1390,7 +1428,7 @@ class ThumbRenderer(QObject):
"""Render a thumbnail or preview image.
Args:
timestamp (float): The timestamp for which this this job was dispatched.
timestamp (float): The timestamp for which this job was dispatched.
filepath (str | Path): The path of the file to render a thumbnail for.
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
pixel_ratio (float): The screen pixel ratio.
@@ -1503,7 +1541,7 @@ class ThumbRenderer(QObject):
save_to_file=file_name,
)
# If the normal renderer failed, fallback the the defaults
# If the normal renderer failed, fallback the defaults
# (with native non-cached sizing!)
if not image:
image = (
@@ -1600,7 +1638,7 @@ class ThumbRenderer(QObject):
"""Render a thumbnail or preview image.
Args:
timestamp (float): The timestamp for which this this job was dispatched.
timestamp (float): The timestamp for which this job was dispatched.
filepath (str | Path): The path of the file to render a thumbnail for.
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
pixel_ratio (float): The screen pixel ratio.
@@ -1703,6 +1741,9 @@ class ThumbRenderer(QObject):
ext, MediaCategories.PDF_TYPES, mime_fallback=True
):
image = self._pdf_thumb(_filepath, adj_size)
# Paint.NET ====================================================
elif MediaCategories.is_ext_in_category(ext, MediaCategories.PAINT_DOT_NET_TYPES):
image = self._pdn_thumb(_filepath)
# No Rendered Thumbnail ========================================
if not image:
raise NoRendererError