* feat: add OpenDocument thumbnail support Co-Authored-By: Josh Beatty <joshuatb6@gmail.com> * 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 <joshuatb6@gmail.com>
@@ -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",
|
||||
@@ -417,6 +430,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,
|
||||
@@ -487,6 +505,7 @@ class MediaCategories:
|
||||
INSTALLER_TYPES,
|
||||
MATERIAL_TYPES,
|
||||
MODEL_TYPES,
|
||||
OPEN_DOCUMENT_TYPES,
|
||||
PACKAGE_TYPES,
|
||||
PDF_TYPES,
|
||||
PLAINTEXT_TYPES,
|
||||
|
||||
@@ -617,7 +617,31 @@ class ThumbRenderer(QObject):
|
||||
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 @@ class ThumbRenderer(QObject):
|
||||
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 @@ class ThumbRenderer(QObject):
|
||||
|
||||
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 @@ class ThumbRenderer(QObject):
|
||||
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
|
||||
|
||||
BIN
tagstudio/tests/fixtures/sample.ods
vendored
Normal file
BIN
tagstudio/tests/fixtures/sample.odt
vendored
Normal file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -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")
|
||||
|
||||