mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
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:
@@ -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" } |
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user