feat: render .clip thumbnails. (#1150)

* feat: render .clip thumbnails.

* doc: document .clip support.

* fix: add CLIP_STUDIO_PAINT_TYPES to ALL_CATEGORIES.

* explicitly close connection.
This commit is contained in:
Sola-ris
2025-12-15 19:12:32 +01:00
committed by GitHub
parent f3bcb7c5c6
commit 7ae3a6bec8
3 changed files with 46 additions and 1 deletions

View File

@@ -79,8 +79,9 @@ Audio thumbnails will default to embedded cover art (if any) and fallback to gen
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.
| Filetype | Extensions | Preview Type |
| ------------------------------------ | --------------------- | -------------------------------------------------------------------------- |
|--------------------------------------| --------------------- | -------------------------------------------------------------------------- |
| Blender | `.blend`, `.blend<#>` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Clip Studio Paint | `.clip` | Embedded thumbnail |
| Keynote (Apple iWork) | `.key` | Embedded thumbnail |
| Krita[^3] | `.kra`, `.krz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } |
| Mdipack (FireAlpaca, Medibang Paint) | `.mdp` | Embedded thumbnail |

View File

@@ -33,6 +33,7 @@ class MediaType(str, Enum):
AUDIO_MIDI = "audio_midi"
AUDIO = "audio"
BLENDER = "blender"
CLIP_STUDIO_PAINT = "clip_studio_paint"
CODE = "code"
DATABASE = "database"
DISK_IMAGE = "disk_image"
@@ -177,6 +178,7 @@ class MediaCategories:
".blend31",
".blend32",
}
_CLIP_STUDIO_PAINT_SET: set[str] = {".clip"}
_CODE_SET: set[str] = {
".bat",
".cfg",
@@ -456,6 +458,12 @@ class MediaCategories:
is_iana=False,
name="blender",
)
CLIP_STUDIO_PAINT_TYPES = MediaCategory(
media_type=MediaType.CLIP_STUDIO_PAINT,
extensions=_CLIP_STUDIO_PAINT_SET,
is_iana=False,
name="clip studio paint",
)
CODE_TYPES = MediaCategory(
media_type=MediaType.CODE,
extensions=_CODE_SET,
@@ -644,6 +652,7 @@ class MediaCategories:
AUDIO_MIDI_TYPES,
AUDIO_TYPES,
BLENDER_TYPES,
CLIP_STUDIO_PAINT_TYPES,
DATABASE_TYPES,
DISK_IMAGE_TYPES,
DOCUMENT_TYPES,

View File

@@ -8,6 +8,7 @@ import contextlib
import hashlib
import math
import os
import sqlite3
import struct
import tarfile
import xml.etree.ElementTree as ET
@@ -1458,6 +1459,35 @@ class ThumbRenderer(QObject):
return im
@staticmethod
def _clip_thumb(filepath: Path) -> Image.Image | None:
"""Extract the thumbnail from the SQLite database embedded in a .clip file.
Args:
filepath (Path): The path of the .clip file.
Returns:
Image: The embedded thumbnail, if extractable.
"""
im: Image.Image | None = None
try:
with open(filepath, "rb") as f:
blob = f.read()
sqlite_index = blob.find(b"SQLite format 3")
if sqlite_index == -1:
return im
with sqlite3.connect(":memory:") as conn:
conn.deserialize(blob[sqlite_index:])
thumbnail = conn.execute("SELECT ImageData FROM CanvasPreview").fetchone()
if thumbnail:
im = Image.open(BytesIO(thumbnail[0]))
conn.close()
except Exception as e:
logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__)
return im
def render(
self,
timestamp: float,
@@ -1708,6 +1738,11 @@ class ThumbRenderer(QObject):
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
):
image = self._krita_thumb(_filepath)
# Clip Studio Paint ============================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.CLIP_STUDIO_PAINT_TYPES
):
image = self._clip_thumb(_filepath)
# VTF ==========================================================
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True