diff --git a/tagstudio/src/core/media_types.py b/tagstudio/src/core/media_types.py index bb1907009..04ea5e872 100644 --- a/tagstudio/src/core/media_types.py +++ b/tagstudio/src/core/media_types.py @@ -23,6 +23,7 @@ class MediaType(str, Enum): DATABASE: str = "database" DISK_IMAGE: str = "disk_image" DOCUMENT: str = "document" + EBOOK: str = "ebook" FONT: str = "font" IMAGE_ANIMATED: str = "image_animated" IMAGE_RAW: str = "image_raw" @@ -160,6 +161,25 @@ class MediaCategories: ".wpd", ".wps", } + _EBOOK_SET: set[str] = { + ".epub", + # ".azw", + # ".azw3", + # ".cb7", + # ".cba", + # ".cbr", + # ".cbt", + # ".cbz", + # ".djvu", + # ".fb2", + # ".ibook", + # ".inf", + # ".kfx", + # ".lit", + # ".mobi", + # ".pdb" + # ".prc", + } _FONT_SET: set[str] = { ".fon", ".otf", @@ -347,6 +367,11 @@ class MediaCategories: extensions=_DOCUMENT_SET, is_iana=False, ) + EBOOK_TYPES: MediaCategory = MediaCategory( + media_type=MediaType.EBOOK, + extensions=_EBOOK_SET, + is_iana=False, + ) FONT_TYPES: MediaCategory = MediaCategory( media_type=MediaType.FONT, extensions=_FONT_SET, @@ -448,6 +473,7 @@ class MediaCategories: DATABASE_TYPES, DISK_IMAGE_TYPES, DOCUMENT_TYPES, + EBOOK_TYPES, FONT_TYPES, IMAGE_ANIMATED_TYPES, IMAGE_RAW_TYPES, diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 2818f3780..7cac38fcd 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -5,6 +5,7 @@ import math import struct +import zipfile from copy import deepcopy from io import BytesIO from pathlib import Path @@ -616,6 +617,29 @@ 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: + """Extracts and returns the first image found in the ePub file at the given filepath. + + Args: + filepath (Path): The path to the ePub file. + + Returns: + Image: The first image found in the ePub file, or None by default. + """ + im: Image.Image = None + try: + with zipfile.ZipFile(filepath, "r") as zip_file: + for file_name in zip_file.namelist(): + if file_name.lower().endswith( + (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg") + ): + image_data = zip_file.read(file_name) + im = Image.open(BytesIO(image_data)) + except Exception as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=e) + + return im + def _font_short_thumb(self, filepath: Path, size: int) -> Image.Image: """Render a small font preview ("Aa") thumbnail from a font file. @@ -1045,6 +1069,11 @@ def render( image = self._audio_waveform_thumb(_filepath, ext, adj_size, pixel_ratio) if image is not None: image = self._apply_overlay_color(image, UiColor.GREEN) + # Ebooks ======================================================= + elif MediaCategories.is_ext_in_category( + ext, MediaCategories.EBOOK_TYPES, mime_fallback=True + ): + image = self._epub_cover(_filepath) # Blender ====================================================== elif MediaCategories.is_ext_in_category( ext, MediaCategories.BLENDER_TYPES, mime_fallback=True @@ -1060,7 +1089,6 @@ def render( ext, MediaCategories.SOURCE_ENGINE_TYPES, mime_fallback=True ): image = self._source_engine(_filepath) - # No Rendered Thumbnail ======================================== if not _filepath.exists(): raise FileNotFoundError diff --git a/tagstudio/tests/fixtures/sample.epub b/tagstudio/tests/fixtures/sample.epub new file mode 100644 index 000000000..b625b67b2 Binary files /dev/null and b/tagstudio/tests/fixtures/sample.epub differ diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_epub_preview.png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_epub_preview.png new file mode 100644 index 000000000..2b5a25815 Binary files /dev/null and b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_epub_preview.png differ diff --git a/tagstudio/tests/qt/test_thumb_renderer.py b/tagstudio/tests/qt/test_thumb_renderer.py index c4f794d4b..f3679629d 100644 --- a/tagstudio/tests/qt/test_thumb_renderer.py +++ b/tagstudio/tests/qt/test_thumb_renderer.py @@ -10,6 +10,18 @@ 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() @@ -18,6 +30,7 @@ def test_pdf_preview(cwd, snapshot): img_bytes = io.BytesIO() img.save(img_bytes, format="PNG") img_bytes.seek(0) + assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension) @@ -29,4 +42,5 @@ def test_svg_preview(cwd, snapshot): img_bytes = io.BytesIO() img.save(img_bytes, format="PNG") img_bytes.seek(0) + assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)