Skip to content

Commit b0c72d0

Browse files
committed
feat: render .pdn thumbnails.
1 parent 1981b13 commit b0c72d0

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

src/tagstudio/core/media_types.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class MediaType(str, Enum):
4949
MODEL = "model"
5050
OPEN_DOCUMENT = "open_document"
5151
PACKAGE = "package"
52+
PAINT_DOT_NET = "paint_dot_net"
5253
PDF = "pdf"
5354
PLAINTEXT = "plaintext"
5455
PRESENTATION = "presentation"
@@ -358,6 +359,7 @@ class MediaCategories:
358359
".pkg",
359360
".xapk",
360361
}
362+
_PAINT_DOT_NET_SET: set[str] = {".pdn"}
361363
_PDF_SET: set[str] = {".pdf"}
362364
_PLAINTEXT_SET: set[str] = {
363365
".csv",
@@ -554,6 +556,12 @@ class MediaCategories:
554556
is_iana=False,
555557
name="package",
556558
)
559+
PAINT_DOT_NET_TYPES = MediaCategory(
560+
media_type=MediaType.PAINT_DOT_NET,
561+
extensions=_PAINT_DOT_NET_SET,
562+
is_iana=False,
563+
name="paint.net",
564+
)
557565
PDF_TYPES = MediaCategory(
558566
media_type=MediaType.PDF,
559567
extensions=_PDF_SET,
@@ -679,7 +687,7 @@ def is_ext_in_category(ext: str, media_cat: MediaCategory, mime_fallback: bool =
679687
680688
Args:
681689
ext (str): File extension with a leading "." and in all lowercase.
682-
media_cat (MediaCategory): The MediaCategory to to check for extension membership.
690+
media_cat (MediaCategory): The MediaCategory to check for extension membership.
683691
mime_fallback (bool): Flag to guess MIME type if no set matches are made.
684692
"""
685693
return media_cat.contains(ext, mime_fallback)

src/tagstudio/qt/previews/renderer.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
44

55

6+
import base64
67
import contextlib
78
import hashlib
89
import math
910
import os
11+
import struct
1012
import tarfile
1113
import xml.etree.ElementTree as ET
1214
import zipfile
@@ -1378,6 +1380,38 @@ def _video_thumb(filepath: Path) -> Image.Image | None:
13781380
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
13791381
return im
13801382

1383+
@staticmethod
1384+
def _pdn_thumb(filepath: Path) -> Image.Image | None:
1385+
"""Extract the base64-encoded thumbnail from a .pdn file header.
1386+
1387+
Args:
1388+
filepath (Path): The path of the .pdn file.
1389+
1390+
Returns:
1391+
Image: the decoded PNG thumbnail or None by default.
1392+
"""
1393+
im: Image.Image | None = None
1394+
with open(filepath, "rb") as f:
1395+
try:
1396+
# First 4 bytes are the magic number
1397+
if f.read(4) != b"PDN3":
1398+
return im
1399+
1400+
# Header length is a little-endian 24-bit int
1401+
header_size = struct.unpack("<i", f.read(3) + b"\x00")[0]
1402+
thumb_element = ET.fromstring(f.read(header_size)).find("./*thumb")
1403+
if thumb_element is None:
1404+
return im
1405+
1406+
encoded_png = thumb_element.get("png")
1407+
if encoded_png:
1408+
decoded_png = base64.b64decode(encoded_png)
1409+
im = Image.open(BytesIO(decoded_png))
1410+
except Exception as e:
1411+
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
1412+
1413+
return im
1414+
13811415
def render(
13821416
self,
13831417
timestamp: float,
@@ -1391,7 +1425,7 @@ def render(
13911425
"""Render a thumbnail or preview image.
13921426
13931427
Args:
1394-
timestamp (float): The timestamp for which this this job was dispatched.
1428+
timestamp (float): The timestamp for which this job was dispatched.
13951429
filepath (str | Path): The path of the file to render a thumbnail for.
13961430
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
13971431
pixel_ratio (float): The screen pixel ratio.
@@ -1504,7 +1538,7 @@ def fetch_cached_image(file_name: Path):
15041538
save_to_file=file_name,
15051539
)
15061540

1507-
# If the normal renderer failed, fallback the the defaults
1541+
# If the normal renderer failed, fallback the defaults
15081542
# (with native non-cached sizing!)
15091543
if not image:
15101544
image = (
@@ -1601,7 +1635,7 @@ def _render(
16011635
"""Render a thumbnail or preview image.
16021636
16031637
Args:
1604-
timestamp (float): The timestamp for which this this job was dispatched.
1638+
timestamp (float): The timestamp for which this job was dispatched.
16051639
filepath (str | Path): The path of the file to render a thumbnail for.
16061640
base_size (tuple[int,int]): The unmodified base size of the thumbnail.
16071641
pixel_ratio (float): The screen pixel ratio.
@@ -1704,6 +1738,9 @@ def _render(
17041738
ext, MediaCategories.PDF_TYPES, mime_fallback=True
17051739
):
17061740
image = self._pdf_thumb(_filepath, adj_size)
1741+
# Paint.NET ====================================================
1742+
elif MediaCategories.is_ext_in_category(ext, MediaCategories.PAINT_DOT_NET_TYPES):
1743+
image = self._pdn_thumb(_filepath)
17071744
# No Rendered Thumbnail ========================================
17081745
if not image:
17091746
raise NoRendererError

0 commit comments

Comments
 (0)