Skip to content

Commit d20be9a

Browse files
committed
feat: render .mdp thumbnails.
1 parent 615978e commit d20be9a

File tree

3 files changed

+71
-14
lines changed

3 files changed

+71
-14
lines changed

docs/preview-support.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,21 @@ 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-
| PDF | `.pdf` | First page render |
93-
| Photoshop | `.psd` | Flattened image render |
94-
| 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+
| 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" } |
9596

9697
### :material-book: eBooks
9798

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"
@@ -335,6 +336,7 @@ class MediaCategories:
335336
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
336337
_IWORK_SET: set[str] = {".key", ".pages", ".numbers"}
337338
_MATERIAL_SET: set[str] = {".mtl"}
339+
_MDIPACK_SET: set[str] = {".mdp"}
338340
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
339341
_OPEN_DOCUMENT_SET: set[str] = {
340342
".fodg",
@@ -536,6 +538,12 @@ class MediaCategories:
536538
is_iana=False,
537539
name="material",
538540
)
541+
MDIPACK_TYPES = MediaCategory(
542+
media_type=MediaType.MDIPACK,
543+
extensions=_MDIPACK_SET,
544+
is_iana=False,
545+
name="mdipack",
546+
)
539547
MODEL_TYPES = MediaCategory(
540548
media_type=MediaType.MODEL,
541549
extensions=_MODEL_SET,
@@ -640,6 +648,7 @@ class MediaCategories:
640648
INSTALLER_TYPES,
641649
IWORK_TYPES,
642650
MATERIAL_TYPES,
651+
MDIPACK_TYPES,
643652
MODEL_TYPES,
644653
OPEN_DOCUMENT_TYPES,
645654
PACKAGE_TYPES,

src/tagstudio/qt/previews/renderer.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
import hashlib
88
import math
99
import os
10+
import struct
1011
import tarfile
1112
import xml.etree.ElementTree as ET
1213
import zipfile
14+
import zlib
1315
from copy import deepcopy
1416
from io import BytesIO
1517
from pathlib import Path
@@ -1378,6 +1380,48 @@ 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 _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+
13811425
def render(
13821426
self,
13831427
timestamp: float,
@@ -1704,6 +1748,9 @@ def _render(
17041748
ext, MediaCategories.PDF_TYPES, mime_fallback=True
17051749
):
17061750
image = self._pdf_thumb(_filepath, adj_size)
1751+
# MDIPACK ======================================================
1752+
elif MediaCategories.is_ext_in_category(ext, MediaCategories.MDIPACK_TYPES):
1753+
image = self._mdp_thumb(_filepath)
17071754
# No Rendered Thumbnail ========================================
17081755
if not image:
17091756
raise NoRendererError

0 commit comments

Comments
 (0)