diff --git a/docs/library/index.md b/docs/library/index.md index 0b7d6718b..947db46b7 100644 --- a/docs/library/index.md +++ b/docs/library/index.md @@ -89,6 +89,7 @@ Preview support for office documents or well-known project file formats varies b | 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" } | | MuseScore | `.mscz` | Embedded thumbnail :material-alert-circle:{ title="If available in file" } | diff --git a/src/tagstudio/core/media_types.py b/src/tagstudio/core/media_types.py index 8659c389d..751e5eeae 100644 --- a/src/tagstudio/core/media_types.py +++ b/src/tagstudio/core/media_types.py @@ -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" @@ -175,6 +176,7 @@ class MediaCategories: ".blend31", ".blend32", } + _CLIP_STUDIO_PAINT_SET: set[str] = {".clip"} _CODE_SET: set[str] = { ".bat", ".cfg", @@ -452,6 +454,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, @@ -628,6 +636,7 @@ class MediaCategories: AUDIO_MIDI_TYPES, AUDIO_TYPES, BLENDER_TYPES, + CLIP_STUDIO_PAINT_TYPES, DATABASE_TYPES, DISK_IMAGE_TYPES, DOCUMENT_TYPES, diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index f47d534ac..fd14fe8a9 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -7,6 +7,7 @@ import hashlib import math import os +import sqlite3 import tarfile import xml.etree.ElementTree as ET import zipfile @@ -1378,6 +1379,34 @@ def _video_thumb(filepath: Path) -> Image.Image | None: logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) 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])) + except Exception as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) + + return im + def render( self, timestamp: float, @@ -1628,6 +1657,11 @@ def _render( 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