refactor: consolidate embedded archive thumbnail functions

This commit is contained in:
Travis Abendshien
2026-07-24 18:56:29 -07:00
parent 068add29df
commit 8995ef6caf
11 changed files with 87 additions and 203 deletions
+3
View File
@@ -1,3 +1,6 @@
# SPDX-FileCopyrightText: (c) TagStudio Contributors
# SPDX-License-Identifier: GPL-3.0-only
from PySide6.QtCore import QObject
+8 -6
View File
@@ -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:
-56
View File
@@ -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
+64 -24
View File
@@ -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)
+4 -4
View File
@@ -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
-35
View File
@@ -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
@@ -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
-35
View File
@@ -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
+4 -3
View File
@@ -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
+2 -2
View File
@@ -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:
+2
View File
@@ -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