Skip to content

Commit 04744b2

Browse files
authored
feat: render .mdp thumbnails. (#1153)
1 parent dcd48eb commit 04744b2

File tree

3 files changed

+71
-15
lines changed

3 files changed

+71
-15
lines changed

docs/preview-support.md

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,21 +78,22 @@ Audio thumbnails will default to embedded cover art (if any) and fallback to gen
7878

7979
Preview support for office documents or well-known project file formats varies by the format and whether or not embedded thumbnails are available to be read from. OpenDocument-based files are typically supported.
8080

81-
| Filetype | Extensions | Preview Type |
82-
| ----------------------------- | --------------------- | -------------------------------------------------------------------------- |
83-
| Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
84-
| Keynote (Apple iWork) | `.key` | Embedded thumbnail |
85-
| Krita[^3] | `.kra`, `.krz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
86-
| MuseScore | `.mscz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
87-
| Numbers (Apple iWork) | `.numbers` | Embedded thumbnail |
88-
| OpenDocument Presentation | `.odp`, `.fodp` | Embedded thumbnail |
89-
| OpenDocument Spreadsheet | `.ods`, `.fods` | Embedded thumbnail |
90-
| OpenDocument Text | `.odt`, `.fodt` | Embedded thumbnail |
91-
| Pages (Apple iWork) | `.pages` | Embedded thumbnail |
92-
| Paint.NET | `.pdn` | Embedded thumbnail |
93-
| PDF | `.pdf` | First page render |
94-
| Photoshop | `.psd` | Flattened image render |
95-
| PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
81+
| Filetype | Extensions | Preview Type |
82+
| ------------------------------------ | --------------------- | -------------------------------------------------------------------------- |
83+
| Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
84+
| Keynote (Apple iWork) | `.key` | Embedded thumbnail |
85+
| Krita[^3] | `.kra`, `.krz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
86+
| Mdipack (FireAlpaca, Medibang Paint) | `.mdp` | Embedded thumbnail |
87+
| MuseScore | `.mscz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
88+
| Numbers (Apple iWork) | `.numbers` | Embedded thumbnail |
89+
| OpenDocument Presentation | `.odp`, `.fodp` | Embedded thumbnail |
90+
| OpenDocument Spreadsheet | `.ods`, `.fods` | Embedded thumbnail |
91+
| OpenDocument Text | `.odt`, `.fodt` | Embedded thumbnail |
92+
| Pages (Apple iWork) | `.pages` | Embedded thumbnail |
93+
| Paint.NET | `.pdn` | Embedded thumbnail |
94+
| PDF | `.pdf` | First page render |
95+
| Photoshop | `.psd` | Flattened image render |
96+
| PowerPoint (Microsoft Office) | `.pptx`, `.ppt` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
9697

9798
### :material-book: eBooks
9899

src/tagstudio/core/media_types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class MediaType(str, Enum):
4646
INSTALLER = "installer"
4747
IWORK = "iwork"
4848
MATERIAL = "material"
49+
MDIPACK = "mdipack"
4950
MODEL = "model"
5051
OPEN_DOCUMENT = "open_document"
5152
PACKAGE = "package"
@@ -336,6 +337,7 @@ class MediaCategories:
336337
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
337338
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
338339
_MATERIAL_SET: set[str] = {".mtl"}
340+
_MDIPACK_SET: set[str] = {".mdp"}
339341
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
340342
_OPEN_DOCUMENT_SET: set[str] = {
341343
".fodg",
@@ -538,6 +540,12 @@ class MediaCategories:
538540
is_iana=False,
539541
name="material",
540542
)
543+
MDIPACK_TYPES = MediaCategory(
544+
media_type=MediaType.MDIPACK,
545+
extensions=_MDIPACK_SET,
546+
is_iana=False,
547+
name="mdipack",
548+
)
541549
MODEL_TYPES = MediaCategory(
542550
media_type=MediaType.MODEL,
543551
extensions=_MODEL_SET,
@@ -648,6 +656,7 @@ class MediaCategories:
648656
INSTALLER_TYPES,
649657
IWORK_TYPES,
650658
MATERIAL_TYPES,
659+
MDIPACK_TYPES,
651660
MODEL_TYPES,
652661
OPEN_DOCUMENT_TYPES,
653662
PACKAGE_TYPES,

src/tagstudio/qt/previews/renderer.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import tarfile
1313
import xml.etree.ElementTree as ET
1414
import zipfile
15+
import zlib
1516
from copy import deepcopy
1617
from io import BytesIO
1718
from pathlib import Path
@@ -1379,6 +1380,48 @@ def _video_thumb(filepath: Path) -> Image.Image | None:
13791380
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
13801381
return im
13811382

1383+
@staticmethod
1384+
def _mdp_thumb(filepath: Path) -> Image.Image | None:
1385+
"""Extract the thumbnail from a .mdp file.
1386+
1387+
Args:
1388+
filepath (Path): The path of the .mdp file.
1389+
1390+
Returns:
1391+
Image: The embedded thumbnail.
1392+
"""
1393+
im: Image.Image | None = None
1394+
try:
1395+
with open(filepath, "rb") as f:
1396+
magic = struct.unpack("<7sx", f.read(8))[0]
1397+
if magic != b"mdipack":
1398+
return im
1399+
1400+
bin_header = struct.unpack("<LLL", f.read(12))
1401+
xml_header = ET.fromstring(f.read(bin_header[1]))
1402+
mdibin_count = len(xml_header.findall("./*Layer")) + 1
1403+
for _ in range(mdibin_count):
1404+
pac_header = struct.unpack("<3sxLLLL48s64s", f.read(132))
1405+
if not pac_header[6].startswith(b"thumb"):
1406+
f.seek(pac_header[3], os.SEEK_CUR)
1407+
continue
1408+
1409+
thumb_element = unwrap(xml_header.find("Thumb"))
1410+
dimensions = (
1411+
int(unwrap(thumb_element.get("width"))),
1412+
int(unwrap(thumb_element.get("height"))),
1413+
)
1414+
thumb_blob = f.read(pac_header[3])
1415+
if pac_header[2] == 1:
1416+
thumb_blob = zlib.decompress(thumb_blob, bufsize=pac_header[4])
1417+
1418+
im = Image.frombytes("RGBA", dimensions, thumb_blob, "raw", "BGRA")
1419+
break
1420+
except Exception as e:
1421+
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
1422+
1423+
return im
1424+
13821425
@staticmethod
13831426
def _pdn_thumb(filepath: Path) -> Image.Image | None:
13841427
"""Extract the base64-encoded thumbnail from a .pdn file header.
@@ -1741,6 +1784,9 @@ def _render(
17411784
ext, MediaCategories.PDF_TYPES, mime_fallback=True
17421785
):
17431786
image = self._pdf_thumb(_filepath, adj_size)
1787+
# MDIPACK ======================================================
1788+
elif MediaCategories.is_ext_in_category(ext, MediaCategories.MDIPACK_TYPES):
1789+
image = self._mdp_thumb(_filepath)
17441790
# Paint.NET ====================================================
17451791
elif MediaCategories.is_ext_in_category(ext, MediaCategories.PAINT_DOT_NET_TYPES):
17461792
image = self._pdn_thumb(_filepath)

0 commit comments

Comments
 (0)