Skip to content

Commit 96026b6

Browse files
feat: add OpenDocument thumbnail support (port #366) (#545)
* feat: add OpenDocument thumbnail support Co-Authored-By: Josh Beatty <[email protected]> * tests: add test comparing odt to png snapshot * tests: add test comparing ods to png snapshot * test: combine OpenDocument tests * test: combine compatible preview tests * test: combine preview render tests * fix: update test snapshots --------- Co-authored-by: Josh Beatty <[email protected]>
1 parent c7171c5 commit 96026b6

10 files changed

+83
-31
lines changed

tagstudio/src/core/media_types.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class MediaType(str, Enum):
3232
INSTALLER: str = "installer"
3333
MATERIAL: str = "material"
3434
MODEL: str = "model"
35+
OPEN_DOCUMENT: str = "open_document"
3536
PACKAGE: str = "package"
3637
PDF: str = "pdf"
3738
PLAINTEXT: str = "plaintext"
@@ -234,6 +235,18 @@ class MediaCategories:
234235
_INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"}
235236
_MATERIAL_SET: set[str] = {".mtl"}
236237
_MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"}
238+
_OPEN_DOCUMENT_SET: set[str] = {
239+
".fodg",
240+
".fodp",
241+
".fods",
242+
".fodt",
243+
".mscz",
244+
".odf",
245+
".odg",
246+
".odp",
247+
".ods",
248+
".odt",
249+
}
237250
_PACKAGE_SET: set[str] = {
238251
".aab",
239252
".akp",
@@ -417,6 +430,11 @@ class MediaCategories:
417430
extensions=_MODEL_SET,
418431
is_iana=True,
419432
)
433+
OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory(
434+
media_type=MediaType.OPEN_DOCUMENT,
435+
extensions=_OPEN_DOCUMENT_SET,
436+
is_iana=False,
437+
)
420438
PACKAGE_TYPES: MediaCategory = MediaCategory(
421439
media_type=MediaType.PACKAGE,
422440
extensions=_PACKAGE_SET,
@@ -487,6 +505,7 @@ class MediaCategories:
487505
INSTALLER_TYPES,
488506
MATERIAL_TYPES,
489507
MODEL_TYPES,
508+
OPEN_DOCUMENT_TYPES,
490509
PACKAGE_TYPES,
491510
PDF_TYPES,
492511
PLAINTEXT_TYPES,

tagstudio/src/qt/widgets/thumb_renderer.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,31 @@ def _source_engine(self, filepath: Path) -> Image.Image:
617617
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
618618
return im
619619

620-
def _epub_cover(self, filepath: Path) -> Image.Image:
620+
@classmethod
621+
def _open_doc_thumb(cls, filepath: Path) -> Image.Image:
622+
"""Extract and render a thumbnail for an OpenDocument file.
623+
624+
Args:
625+
filepath (Path): The path of the file.
626+
"""
627+
file_path_within_zip = "Thumbnails/thumbnail.png"
628+
im: Image.Image = None
629+
with zipfile.ZipFile(filepath, "r") as zip_file:
630+
# Check if the file exists in the zip
631+
if file_path_within_zip in zip_file.namelist():
632+
# Read the specific file into memory
633+
file_data = zip_file.read(file_path_within_zip)
634+
thumb_im = Image.open(BytesIO(file_data))
635+
if thumb_im:
636+
im = Image.new("RGB", thumb_im.size, color="#1e1e1e")
637+
im.paste(thumb_im)
638+
else:
639+
logger.error("Couldn't render thumbnail", filepath=filepath)
640+
641+
return im
642+
643+
@classmethod
644+
def _epub_cover(cls, filepath: Path) -> Image.Image:
621645
"""Extracts and returns the first image found in the ePub file at the given filepath.
622646
623647
Args:
@@ -780,7 +804,8 @@ def _image_thumb(self, filepath: Path) -> Image.Image:
780804
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
781805
return im
782806

783-
def _image_vector_thumb(self, filepath: Path, size: int) -> Image.Image:
807+
@classmethod
808+
def _image_vector_thumb(cls, filepath: Path, size: int) -> Image.Image:
784809
"""Render a thumbnail for a vector image, such as SVG.
785810
786811
Args:
@@ -848,7 +873,8 @@ def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image:
848873

849874
return im
850875

851-
def _pdf_thumb(self, filepath: Path, size: int) -> Image.Image:
876+
@classmethod
877+
def _pdf_thumb(cls, filepath: Path, size: int) -> Image.Image:
852878
"""Render a thumbnail for a PDF file.
853879
854880
filepath (Path): The path of the file.
@@ -1045,6 +1071,11 @@ def render(
10451071
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
10461072
):
10471073
image = self._video_thumb(_filepath)
1074+
# OpenDocument/OpenOffice ======================================
1075+
elif MediaCategories.is_ext_in_category(
1076+
ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True
1077+
):
1078+
image = self._open_doc_thumb(_filepath)
10481079
# Plain Text ===================================================
10491080
elif MediaCategories.is_ext_in_category(
10501081
ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True

tagstudio/tests/fixtures/sample.ods

11 KB
Binary file not shown.

tagstudio/tests/fixtures/sample.odt

14.1 KB
Binary file not shown.

tagstudio/tests/qt/test_thumb_renderer.py

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,43 @@
33
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
44

55
import io
6+
from functools import partial
67
from pathlib import Path
78

9+
import pytest
810
from PIL import Image
911
from src.qt.widgets.thumb_renderer import ThumbRenderer
1012
from syrupy.extensions.image import PNGImageSnapshotExtension
1113

1214

13-
def test_epub_preview(cwd, snapshot):
14-
file_path: Path = cwd / "fixtures" / "sample.epub"
15-
tr = ThumbRenderer()
16-
img: Image.Image = tr._epub_cover(file_path)
17-
18-
img_bytes = io.BytesIO()
19-
img.save(img_bytes, format="PNG")
20-
img_bytes.seek(0)
21-
22-
assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)
23-
24-
25-
def test_pdf_preview(cwd, snapshot):
26-
file_path: Path = cwd / "fixtures" / "sample.pdf"
27-
renderer = ThumbRenderer()
28-
img: Image.Image = renderer._pdf_thumb(file_path, 200)
29-
30-
img_bytes = io.BytesIO()
31-
img.save(img_bytes, format="PNG")
32-
img_bytes.seek(0)
33-
34-
assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)
35-
36-
37-
def test_svg_preview(cwd, snapshot):
38-
file_path: Path = cwd / "fixtures" / "sample.svg"
39-
renderer = ThumbRenderer()
40-
img: Image.Image = renderer._image_vector_thumb(file_path, 200)
15+
@pytest.mark.parametrize(
16+
["fixture_file", "thumbnailer"],
17+
[
18+
(
19+
"sample.odt",
20+
ThumbRenderer._open_doc_thumb,
21+
),
22+
(
23+
"sample.ods",
24+
ThumbRenderer._open_doc_thumb,
25+
),
26+
(
27+
"sample.epub",
28+
ThumbRenderer._epub_cover,
29+
),
30+
(
31+
"sample.pdf",
32+
partial(ThumbRenderer._pdf_thumb, size=200),
33+
),
34+
(
35+
"sample.svg",
36+
partial(ThumbRenderer._image_vector_thumb, size=200),
37+
),
38+
],
39+
)
40+
def test_preview_render(cwd, fixture_file, thumbnailer, snapshot):
41+
file_path: Path = cwd / "fixtures" / fixture_file
42+
img: Image.Image = thumbnailer(file_path)
4143

4244
img_bytes = io.BytesIO()
4345
img.save(img_bytes, format="PNG")

0 commit comments

Comments
 (0)