diff --git a/src/tagstudio/qt/file_renderer.py b/src/tagstudio/qt/file_renderer.py index 2db3e1ce..c7448ada 100644 --- a/src/tagstudio/qt/file_renderer.py +++ b/src/tagstudio/qt/file_renderer.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: (c) TagStudio Contributors +# SPDX-License-Identifier: GPL-3.0-only + from PySide6.QtCore import QObject diff --git a/src/tagstudio/qt/previews/renderer.py b/src/tagstudio/qt/previews/renderer.py index d4285f18..d6546325 100644 --- a/src/tagstudio/qt/previews/renderer.py +++ b/src/tagstudio/qt/previews/renderer.py @@ -36,17 +36,19 @@ from tagstudio.qt.global_settings import ( from tagstudio.qt.helpers.gradients import four_corner_gradient from tagstudio.qt.models.palette import UI_COLORS, ColorType, UiColor, get_ui_color from tagstudio.qt.resource_manager import ResourceManager -from tagstudio.renderers.apple import apple_embedded_thumb -from tagstudio.renderers.archive import archive_thumb +from tagstudio.renderers.archive import ( + apple_embedded_thumb, + archive_thumb, + krita_thumb, + open_doc_thumb, + powerpoint_thumb, +) from tagstudio.renderers.audio import audio_album_thumb, audio_waveform_thumb from tagstudio.renderers.blender import blender from tagstudio.renderers.clip_studio import clip_thumb, pdn_thumb from tagstudio.renderers.ebook import epub_cover from tagstudio.renderers.font import font_long_thumb, font_short_thumb -from tagstudio.renderers.krita import krita_thumb from tagstudio.renderers.medibang_paint import mdp_thumb -from tagstudio.renderers.microsoft_office import powerpoint_thumb -from tagstudio.renderers.open_document import open_doc_thumb from tagstudio.renderers.pdf import pdf_thumb from tagstudio.renderers.raster_image import image_exr_thumb, image_raw_thumb, image_thumb from tagstudio.renderers.source_engine import vtf_thumb @@ -58,10 +60,10 @@ if TYPE_CHECKING: from tagstudio.qt.ts_qt import QtDriver ImageFile.LOAD_TRUNCATED_IMAGES = True +Image.MAX_IMAGE_PIXELS = None logger = structlog.get_logger(__name__) -Image.MAX_IMAGE_PIXELS = None try: diff --git a/src/tagstudio/renderers/apple.py b/src/tagstudio/renderers/apple.py deleted file mode 100644 index 2f93e941..00000000 --- a/src/tagstudio/renderers/apple.py +++ /dev/null @@ -1,56 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import zipfile -from io import BytesIO -from pathlib import Path - -import structlog -from PIL import Image - -logger = structlog.get_logger(__name__) - - -def apple_embedded_thumb(filepath: Path) -> Image.Image | None: - """Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio). - - Args: - filepath (Path): The path of the file. - """ - thumb_files: list[str] = [ - "preview.jpg", - "QuickLook/Preview.heic", - "QuickLook/Thumbnail.jpg", - "QuickLook/Thumbnail.heic", - "QuickLook/Thumbnail.webp", - "QuickLook/Icon.webp", - ] - im: Image.Image | None = None - - def get_image(path: str) -> Image.Image | None: - thumb_im: Image.Image | None = None - # Read the specific file into memory - file_data = zip_file.read(path) - thumb_im = Image.open(BytesIO(file_data)) - return thumb_im - - try: - with zipfile.ZipFile(filepath, "r") as zip_file: - thumb: Image.Image | None = None - - # Check if the file exists in the zip - for thumb_file in thumb_files: - if thumb_file in zip_file.namelist(): - thumb = get_image(thumb_file) - break - else: - logger.error("Couldn't render thumbnail", filepath=filepath) - - if thumb: - im = Image.new("RGB", thumb.size, color="#1e1e1e") - im.paste(thumb) - except zipfile.BadZipFile as e: - logger.error("Couldn't render thumbnail", filepath=filepath, error=e) - - return im diff --git a/src/tagstudio/renderers/archive.py b/src/tagstudio/renderers/archive.py index d6a5b5bb..cf66ac82 100644 --- a/src/tagstudio/renderers/archive.py +++ b/src/tagstudio/renderers/archive.py @@ -16,7 +16,7 @@ from PIL import Image from tagstudio.core.media_types import MediaCategories from tagstudio.core.utils.types import unwrap -from tagstudio.renderers.raster_image import load_raster_image +from tagstudio.renderers.raster_image import image_from_bytes logger = structlog.get_logger(__name__) @@ -60,27 +60,7 @@ class TarFile: self.tar.__exit__(*args) -def archive_thumb(filepath: Path, ext: str) -> Image.Image | None: - """Extract the first image found in the archive. - - Args: - filepath (Path): The path to the archive. - ext (str): The file extension. - - Returns: - Image: The first image found in the archive. - """ - im: Image.Image | None = None - try: - with open_archive(filepath, ext) as archive: - im = first_image(archive) - except Exception as e: - logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) - - return im - - -def open_archive(filepath: Path, ext: str) -> Archive: +def open_archive(filepath: Path, ext: str = "") -> Archive: """Open an archive with its corresponding archiver. Args: @@ -100,7 +80,7 @@ def open_archive(filepath: Path, ext: str) -> Archive: return archiver(filepath, "r") -def first_image(archive: Archive) -> Image.Image | None: +def first_image_in_archive(archive: Archive) -> Image.Image | None: """Find and extract the first renderable image in the archive. Args: @@ -113,6 +93,66 @@ def first_image(archive: Archive) -> Image.Image | None: ext = Path(file_name).suffix if MediaCategories.IMAGE_RASTER_TYPES.contains(ext): image_data = archive.read(file_name) # pyright: ignore[reportUnknownVariableType] - return load_raster_image(BytesIO(image_data)) + return image_from_bytes(BytesIO(image_data)) return None + + +def archive_thumb( + filepath: Path, ext: str = "", image_names: list[Path] | list[str] | None = None +) -> Image.Image | None: + """Extract an embedded preview image from an archive. + + Args: + filepath (Path): The path to the archive. + ext (str): The file extension. Used to help determine more specific archive type. + image_names: (list[Path] | list[str] | None): List of embedded image names to search for. + + Returns: + Image: The first image found in the archive. + """ + try: + with open_archive(filepath, ext) as archive: + # If no list of image names to search for was provided, default to the first image. + if not image_names: + return first_image_in_archive(archive) + + for image_name in image_names: + if image_name in archive.namelist(): + file_data = archive.read(str(image_name)) # pyright: ignore[reportUnknownVariableType] + return image_from_bytes(BytesIO(file_data)) + + except Exception as e: + logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) + return None + + +def apple_embedded_thumb(filepath: Path) -> Image.Image | None: + """Extract and render an apple embedded thumbnail (iWork, Apple Creative Studio).""" + image_names: list[str] = [ + "preview.jpg", + "QuickLook/Preview.heic", + "QuickLook/Thumbnail.jpg", + "QuickLook/Thumbnail.heic", + "QuickLook/Thumbnail.webp", + "QuickLook/Icon.webp", + ] + return archive_thumb(filepath, image_names=image_names) + + +def krita_thumb(filepath: Path) -> Image.Image | None: + """Extract and render a thumbnail for a Krita file.""" + image_names = ["preview.png"] + return archive_thumb(filepath, image_names=image_names) + + +def open_doc_thumb(filepath: Path) -> Image.Image | None: + """Extract and render a thumbnail for an OpenDocument file.""" + image_names = ["Thumbnails/thumbnail.png"] + return archive_thumb(filepath, image_names=image_names) + + +def powerpoint_thumb(filepath: Path) -> Image.Image | None: + """Extract and render a thumbnail for a Microsoft PowerPoint file.""" + image_names = ["docProps/thumbnail.jpeg"] + return archive_thumb(filepath, image_names=image_names) diff --git a/src/tagstudio/renderers/ebook.py b/src/tagstudio/renderers/ebook.py index 7f8a5fea..acc0ab35 100644 --- a/src/tagstudio/renderers/ebook.py +++ b/src/tagstudio/renderers/ebook.py @@ -12,8 +12,8 @@ from PIL import Image from tagstudio.core.media_types import MediaCategories from tagstudio.core.utils.types import unwrap -from tagstudio.renderers.archive import Archive, first_image, open_archive -from tagstudio.renderers.raster_image import load_raster_image +from tagstudio.renderers.archive import Archive, first_image_in_archive, open_archive +from tagstudio.renderers.raster_image import image_from_bytes logger = structlog.get_logger(__name__) @@ -39,7 +39,7 @@ def epub_cover(filepath: Path, ext: str) -> Image.Image | None: im = cover_from_comic_info(archive, comic_info, "InnerCover") if not im: - im = first_image(archive) + im = first_image_in_archive(archive) except Exception as e: logger.error("Couldn't render thumbnail", filepath=filepath, error=type(e).__name__) @@ -68,6 +68,6 @@ def cover_from_comic_info( ext = Path(page_name).suffix if MediaCategories.IMAGE_RASTER_TYPES.contains(ext): image_data = archive.read(page_name) # pyright: ignore[reportUnknownVariableType] - im = load_raster_image(BytesIO(image_data)) + im = image_from_bytes(BytesIO(image_data)) return im diff --git a/src/tagstudio/renderers/krita.py b/src/tagstudio/renderers/krita.py deleted file mode 100644 index bab9e9a0..00000000 --- a/src/tagstudio/renderers/krita.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import zipfile -from io import BytesIO -from pathlib import Path - -import structlog -from PIL import Image - -logger = structlog.get_logger(__name__) - - -def krita_thumb(filepath: Path) -> Image.Image | None: - """Extract and render a thumbnail for a Krita file. - - Args: - filepath (Path): The path of the file. - """ - file_path_within_zip = "preview.png" - im: Image.Image | None = 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 diff --git a/src/tagstudio/renderers/microsoft_office.py b/src/tagstudio/renderers/microsoft_office.py deleted file mode 100644 index 16933b5f..00000000 --- a/src/tagstudio/renderers/microsoft_office.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import zipfile -from io import BytesIO -from pathlib import Path - -import structlog -from PIL import Image - -logger = structlog.get_logger(__name__) - - -def powerpoint_thumb(filepath: Path) -> Image.Image | None: - """Extract and render a thumbnail for a Microsoft PowerPoint file. - - Args: - filepath (Path): The path of the file. - """ - file_path_within_zip = "docProps/thumbnail.jpeg" - im: Image.Image | None = None - try: - 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) - except zipfile.BadZipFile as e: - logger.error("Couldn't render thumbnail", filepath=filepath, error=e) - - return im diff --git a/src/tagstudio/renderers/open_document.py b/src/tagstudio/renderers/open_document.py deleted file mode 100644 index 42f34763..00000000 --- a/src/tagstudio/renderers/open_document.py +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: (c) TagStudio Contributors -# SPDX-License-Identifier: GPL-3.0-only - - -import zipfile -from io import BytesIO -from pathlib import Path - -import structlog -from PIL import Image - -logger = structlog.get_logger(__name__) - - -def open_doc_thumb(filepath: Path) -> Image.Image | None: - """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 = 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 diff --git a/src/tagstudio/renderers/pdf.py b/src/tagstudio/renderers/pdf.py index 5e8ea1b9..39e97b26 100644 --- a/src/tagstudio/renderers/pdf.py +++ b/src/tagstudio/renderers/pdf.py @@ -1,14 +1,15 @@ # SPDX-FileCopyrightText: (c) TagStudio Contributors # SPDX-License-Identifier: GPL-3.0-only +# NOTE: This file contains Qt imports because Qt is used as the vector renderer in this case. +# This is NOT considered part of the Qt frontend, but is technically tangled with the Qt import. + from io import BytesIO from pathlib import Path import structlog -from PIL import ( - Image, -) +from PIL import Image from PySide6.QtCore import QBuffer, QFile, QFileDevice, QIODeviceBase, QSizeF from PySide6.QtGui import QImage from PySide6.QtPdf import QPdfDocument, QPdfDocumentRenderOptions diff --git a/src/tagstudio/renderers/raster_image.py b/src/tagstudio/renderers/raster_image.py index d91077a7..40b0445d 100644 --- a/src/tagstudio/renderers/raster_image.py +++ b/src/tagstudio/renderers/raster_image.py @@ -35,7 +35,7 @@ def image_thumb(filepath: Path) -> Image.Image | None: im: Image.Image | None = None try: with filepath.open("rb") as file: - im = load_raster_image(BytesIO(file.read())) + im = image_from_bytes(BytesIO(file.read())) except ( FileNotFoundError, UnidentifiedImageError, @@ -102,7 +102,7 @@ def image_raw_thumb(filepath: Path) -> Image.Image | None: return im -def load_raster_image(image_data: BytesIO) -> Image.Image: +def image_from_bytes(image_data: BytesIO) -> Image.Image: """Load a raster image and add a background if it's transparent. Args: diff --git a/src/tagstudio/renderers/vector_image.py b/src/tagstudio/renderers/vector_image.py index a79e82ec..dd4d117c 100644 --- a/src/tagstudio/renderers/vector_image.py +++ b/src/tagstudio/renderers/vector_image.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: (c) TagStudio Contributors # SPDX-License-Identifier: GPL-3.0-only +# NOTE: This file contains Qt imports because Qt is used as the vector renderer in this case. +# This is NOT considered part of the Qt frontend, but is technically tangled with the Qt import. from io import BytesIO from pathlib import Path