diff --git a/tagstudio/src/core/media_types.py b/tagstudio/src/core/media_types.py index 04ea5e872..c2ffe0abe 100644 --- a/tagstudio/src/core/media_types.py +++ b/tagstudio/src/core/media_types.py @@ -32,6 +32,7 @@ class MediaType(str, Enum): INSTALLER: str = "installer" MATERIAL: str = "material" MODEL: str = "model" + OPEN_DOCUMENT: str = "open_document" PACKAGE: str = "package" PDF: str = "pdf" PLAINTEXT: str = "plaintext" @@ -234,6 +235,18 @@ class MediaCategories: _INSTALLER_SET: set[str] = {".appx", ".msi", ".msix"} _MATERIAL_SET: set[str] = {".mtl"} _MODEL_SET: set[str] = {".3ds", ".fbx", ".obj", ".stl"} + _OPEN_DOCUMENT_SET: set[str] = { + ".fodg", + ".fodp", + ".fods", + ".fodt", + ".mscz", + ".odf", + ".odg", + ".odp", + ".ods", + ".odt", + } _PACKAGE_SET: set[str] = { ".aab", ".akp", @@ -412,6 +425,11 @@ class MediaCategories: extensions=_MODEL_SET, is_iana=True, ) + OPEN_DOCUMENT_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.OPEN_DOCUMENT, + extensions=_OPEN_DOCUMENT_SET, + is_iana=False, + ) PACKAGE_TYPES: MediaCategory = MediaCategory( media_type=MediaType.PACKAGE, extensions=_PACKAGE_SET, @@ -482,6 +500,7 @@ class MediaCategories: INSTALLER_TYPES, MATERIAL_TYPES, MODEL_TYPES, + OPEN_DOCUMENT_TYPES, PACKAGE_TYPES, PDF_TYPES, PLAINTEXT_TYPES, diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 7cac38fcd..348ab66b8 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -617,7 +617,31 @@ def _source_engine(self, filepath: Path) -> Image.Image: logger.error("Couldn't render thumbnail", filepath=filepath, error=e) return im - def _epub_cover(self, filepath: Path) -> Image.Image: + @classmethod + def _open_doc_thumb(cls, filepath: Path) -> Image.Image: + """Extract and render a thumbnail for an OpenDocument file. + + Args: + filepath (Path): The path of the file. + """ + file_path_within_zip = "Thumbnails/thumbnail.png" + im: Image.Image = None + with zipfile.ZipFile(filepath, "r") as zip_file: + # Check if the file exists in the zip + if file_path_within_zip in zip_file.namelist(): + # Read the specific file into memory + file_data = zip_file.read(file_path_within_zip) + thumb_im = Image.open(BytesIO(file_data)) + if thumb_im: + im = Image.new("RGB", thumb_im.size, color="#1e1e1e") + im.paste(thumb_im) + else: + logger.error("Couldn't render thumbnail", filepath=filepath) + + return im + + @classmethod + def _epub_cover(cls, filepath: Path) -> Image.Image: """Extracts and returns the first image found in the ePub file at the given filepath. Args: @@ -780,7 +804,8 @@ def _image_thumb(self, filepath: Path) -> Image.Image: logger.error("Couldn't render thumbnail", filepath=filepath, error=e) return im - def _image_vector_thumb(self, filepath: Path, size: int) -> Image.Image: + @classmethod + def _image_vector_thumb(cls, filepath: Path, size: int) -> Image.Image: """Render a thumbnail for a vector image, such as SVG. Args: @@ -848,7 +873,8 @@ def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image: return im - def _pdf_thumb(self, filepath: Path, size: int) -> Image.Image: + @classmethod + def _pdf_thumb(cls, filepath: Path, size: int) -> Image.Image: """Render a thumbnail for a PDF file. filepath (Path): The path of the file. @@ -1045,6 +1071,11 @@ def render( ext, MediaCategories.VIDEO_TYPES, mime_fallback=True ): image = self._video_thumb(_filepath) + # OpenDocument/OpenOffice ====================================== + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.OPEN_DOCUMENT_TYPES, mime_fallback=True + ): + image = self._open_doc_thumb(_filepath) # Plain Text =================================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.PLAINTEXT_TYPES, mime_fallback=True diff --git a/tagstudio/tests/fixtures/sample.ods b/tagstudio/tests/fixtures/sample.ods new file mode 100644 index 000000000..ecc97b12f Binary files /dev/null and b/tagstudio/tests/fixtures/sample.ods differ diff --git a/tagstudio/tests/fixtures/sample.odt b/tagstudio/tests/fixtures/sample.odt new file mode 100644 index 000000000..4cb6f2f1c Binary files /dev/null and b/tagstudio/tests/fixtures/sample.odt differ diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_epub_preview.png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.epub-_epub_cover].png similarity index 100% rename from tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_epub_preview.png rename to tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.epub-_epub_cover].png diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png new file mode 100644 index 000000000..5e749f2d3 Binary files /dev/null and b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.ods-_open_doc_thumb].png differ diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png new file mode 100644 index 000000000..ac2158e7d Binary files /dev/null and b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.odt-_open_doc_thumb].png differ diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_pdf_preview.png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.pdf-thumbnailer3].png similarity index 100% rename from tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_pdf_preview.png rename to tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.pdf-thumbnailer3].png diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_svg_preview.png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.svg-thumbnailer4].png similarity index 100% rename from tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_svg_preview.png rename to tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_preview_render[sample.svg-thumbnailer4].png diff --git a/tagstudio/tests/qt/test_thumb_renderer.py b/tagstudio/tests/qt/test_thumb_renderer.py index f3679629d..8290ff054 100644 --- a/tagstudio/tests/qt/test_thumb_renderer.py +++ b/tagstudio/tests/qt/test_thumb_renderer.py @@ -3,41 +3,43 @@ # Created for TagStudio: https://github.com/CyanVoxel/TagStudio import io +from functools import partial from pathlib import Path +import pytest from PIL import Image from src.qt.widgets.thumb_renderer import ThumbRenderer from syrupy.extensions.image import PNGImageSnapshotExtension -def test_epub_preview(cwd, snapshot): - file_path: Path = cwd / "fixtures" / "sample.epub" - tr = ThumbRenderer() - img: Image.Image = tr._epub_cover(file_path) - - img_bytes = io.BytesIO() - img.save(img_bytes, format="PNG") - img_bytes.seek(0) - - assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension) - - -def test_pdf_preview(cwd, snapshot): - file_path: Path = cwd / "fixtures" / "sample.pdf" - renderer = ThumbRenderer() - img: Image.Image = renderer._pdf_thumb(file_path, 200) - - img_bytes = io.BytesIO() - img.save(img_bytes, format="PNG") - img_bytes.seek(0) - - assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension) - - -def test_svg_preview(cwd, snapshot): - file_path: Path = cwd / "fixtures" / "sample.svg" - renderer = ThumbRenderer() - img: Image.Image = renderer._image_vector_thumb(file_path, 200) +@pytest.mark.parametrize( + ["fixture_file", "thumbnailer"], + [ + ( + "sample.odt", + ThumbRenderer._open_doc_thumb, + ), + ( + "sample.ods", + ThumbRenderer._open_doc_thumb, + ), + ( + "sample.epub", + ThumbRenderer._epub_cover, + ), + ( + "sample.pdf", + partial(ThumbRenderer._pdf_thumb, size=200), + ), + ( + "sample.svg", + partial(ThumbRenderer._image_vector_thumb, size=200), + ), + ], +) +def test_preview_render(cwd, fixture_file, thumbnailer, snapshot): + file_path: Path = cwd / "fixtures" / fixture_file + img: Image.Image = thumbnailer(file_path) img_bytes = io.BytesIO() img.save(img_bytes, format="PNG")