From 9255a86ad1d0800e663fb984065dd7e3362b2916 Mon Sep 17 00:00:00 2001 From: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:30:46 -0700 Subject: [PATCH] feat: add svg thumbnail support (port #442) (#540) * feat: add svg thumbnail support Co-Authored-By: Tyrannicodin <86689800+Tyrannicodin@users.noreply.github.com> * flip `svg.isValid()` logic check * tests: add test comparing svg to png snapshot Co-Authored-By: yed --------- Co-authored-by: Tyrannicodin <86689800+Tyrannicodin@users.noreply.github.com> Co-authored-by: yed --- tagstudio/src/qt/widgets/thumb_renderer.py | 33 ++++++++++++++++-- tagstudio/tests/fixtures/sample.svg | 8 +++++ .../test_thumb_renderer/test_svg_preview.png | Bin 0 -> 5248 bytes tagstudio/tests/qt/test_thumb_renderer.py | 21 +++++++++++ 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 tagstudio/tests/fixtures/sample.svg create mode 100644 tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_svg_preview.png create mode 100644 tagstudio/tests/qt/test_thumb_renderer.py diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index 83eb1c579..377a5f730 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -28,8 +28,9 @@ from PIL.Image import DecompressionBombError from pillow_heif import register_avif_opener, register_heif_opener from pydub import exceptions -from PySide6.QtCore import QObject, QSize, Qt, Signal -from PySide6.QtGui import QGuiApplication, QPixmap +from PySide6.QtCore import QBuffer, QObject, QSize, Qt, Signal +from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPixmap +from PySide6.QtSvg import QSvgRenderer from src.core.constants import FONT_SAMPLE_SIZES, FONT_SAMPLE_TEXT from src.core.media_types import MediaCategories, MediaType from src.core.palette import ColorType, UiColor, get_ui_color @@ -750,8 +751,33 @@ def _image_vector_thumb(self, filepath: Path, size: int) -> Image.Image: filepath (Path): The path of the file. size (tuple[int,int]): The size of the thumbnail. """ - # TODO: Implement. im: Image.Image = None + # Create an image to draw the svg to and a painter to do the drawing + image: QImage = QImage(size, size, QImage.Format.Format_ARGB32) + image.fill("#1e1e1e") + + # Create an svg renderer, then render to the painter + svg: QSvgRenderer = QSvgRenderer(str(filepath)) + + if not svg.isValid(): + raise UnidentifiedImageError + + painter: QPainter = QPainter(image) + svg.setAspectRatioMode(Qt.AspectRatioMode.KeepAspectRatio) + svg.render(painter) + painter.end() + + # Write the image to a buffer as png + buffer: QBuffer = QBuffer() + buffer.open(QBuffer.OpenModeFlag.ReadWrite) + image.save(buffer, "PNG") + + # Load the image from the buffer + im = Image.new("RGB", (size, size), color="#1e1e1e") + im.paste(Image.open(BytesIO(buffer.data().data()))) + im = im.convert(mode="RGB") + + buffer.close() return im def _model_stl_thumb(self, filepath: Path, size: int) -> Image.Image: @@ -924,6 +950,7 @@ def render( ext, MediaCategories.IMAGE_RAW_TYPES, mime_fallback=True ): image = self._image_raw_thumb(_filepath) + # Vector Images -------------------------------------------- elif MediaCategories.is_ext_in_category( ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True ): diff --git a/tagstudio/tests/fixtures/sample.svg b/tagstudio/tests/fixtures/sample.svg new file mode 100644 index 000000000..99c924a86 --- /dev/null +++ b/tagstudio/tests/fixtures/sample.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_svg_preview.png b/tagstudio/tests/qt/__snapshots__/test_thumb_renderer/test_svg_preview.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd90431406e2658126b2619644d61f4940091b1 GIT binary patch literal 5248 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 literal 0 HcmV?d00001 diff --git a/tagstudio/tests/qt/test_thumb_renderer.py b/tagstudio/tests/qt/test_thumb_renderer.py new file mode 100644 index 000000000..c9f675955 --- /dev/null +++ b/tagstudio/tests/qt/test_thumb_renderer.py @@ -0,0 +1,21 @@ +# Copyright (C) 2024 Travis Abendshien (CyanVoxel). +# Licensed under the GPL-3.0 License. +# Created for TagStudio: https://github.com/CyanVoxel/TagStudio + +import io +from pathlib import Path + +from PIL import Image +from src.qt.widgets.thumb_renderer import ThumbRenderer +from syrupy.extensions.image import 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) + + img_bytes = io.BytesIO() + img.save(img_bytes, format="PNG") + img_bytes.seek(0) + assert img_bytes.read() == snapshot(extension_class=PNGImageSnapshotExtension)