33# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
44
55
6+ import base64
67import contextlib
78import hashlib
89import math
910import os
11+ import struct
1012import tarfile
1113import xml .etree .ElementTree as ET
1214import 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