mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-28 22:01:24 +00:00
feat: render .cbr thumbnails. (#1112)
This commit is contained in:
@@ -219,6 +219,20 @@ Don't forget to rebuild!
|
||||
|
||||
For audio/video thumbnails and playback you'll need [FFmpeg](https://ffmpeg.org/download.html) installed on your system. If you encounter any issues with this, please reference our [FFmpeg Help](./help/ffmpeg.md) guide.
|
||||
|
||||
### RAR extractor
|
||||
|
||||
To generate thumbnails for RAR-based files (like `.cbr`) you'll need an extractor capable of handling them.
|
||||
|
||||
On Linux you'll need to install either `unrar` (likely in you distro's non-free repository) or `unrar-free` from your package manager.
|
||||
|
||||
On Mac `unrar` can be installed through Homebrew's [`rar`](https://formulae.brew.sh/cask/rar) formula.
|
||||
|
||||
On Windows you'll need to install either [`WinRAR`](https://www.rarlab.com/download.htm) or [`7-zip`](https://www.7-zip.org/) and add their folder to you `PATH`.
|
||||
|
||||
#### Note
|
||||
|
||||
Both `unrar` and `WinRAR` require a license, but since the evaluation copy has no time limit you can simply dismiss the prompt.
|
||||
|
||||
### ripgrep
|
||||
|
||||
A recommended tool to improve the performance of directory scanning is [`ripgrep`](https://github.com/BurntSushi/ripgrep), a Rust-based directory walker that natively integrates with our [`.ts_ignore`](./utilities/ignore.md) (`.gitignore`-style) pattern matching system for excluding files and directories. Ripgrep is already pre-installed on some Linux distributions and also available from several package managers.
|
||||
|
||||
@@ -23,6 +23,7 @@ dependencies = [
|
||||
"pydantic~=2.10",
|
||||
"pydub~=0.25",
|
||||
"PySide6==6.8.0.*",
|
||||
"rarfile==4.2",
|
||||
"rawpy~=0.24",
|
||||
"Send2Trash~=1.8",
|
||||
"SQLAlchemy~=2.0",
|
||||
|
||||
@@ -19,6 +19,7 @@ from xml.etree.ElementTree import Element
|
||||
import cv2
|
||||
import numpy as np
|
||||
import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport]
|
||||
import rarfile
|
||||
import rawpy
|
||||
import srctools
|
||||
import structlog
|
||||
@@ -857,11 +858,12 @@ class ThumbRenderer(QObject):
|
||||
return im
|
||||
|
||||
@staticmethod
|
||||
def _epub_cover(filepath: Path) -> Image.Image | None:
|
||||
def _epub_cover(filepath: Path, ext: str) -> Image.Image | None:
|
||||
"""Extracts the cover specified by ComicInfo.xml or first image found in the ePub file.
|
||||
|
||||
Args:
|
||||
filepath (Path): The path to the ePub file.
|
||||
ext (str): The file extension.
|
||||
|
||||
Returns:
|
||||
Image: The cover specified in ComicInfo.xml,
|
||||
@@ -869,21 +871,25 @@ class ThumbRenderer(QObject):
|
||||
"""
|
||||
im: Image.Image | None = None
|
||||
try:
|
||||
with zipfile.ZipFile(filepath, "r") as zip_file:
|
||||
if "ComicInfo.xml" in zip_file.namelist():
|
||||
comic_info = ET.fromstring(zip_file.read("ComicInfo.xml"))
|
||||
im = ThumbRenderer.__cover_from_comic_info(zip_file, comic_info, "FrontCover")
|
||||
archiver: type[zipfile.ZipFile] | type[rarfile.RarFile] = zipfile.ZipFile
|
||||
if ext == ".cbr":
|
||||
archiver = rarfile.RarFile
|
||||
|
||||
with archiver(filepath, "r") as archive:
|
||||
if "ComicInfo.xml" in archive.namelist():
|
||||
comic_info = ET.fromstring(archive.read("ComicInfo.xml"))
|
||||
im = ThumbRenderer.__cover_from_comic_info(archive, comic_info, "FrontCover")
|
||||
if not im:
|
||||
im = ThumbRenderer.__cover_from_comic_info(
|
||||
zip_file, comic_info, "InnerCover"
|
||||
archive, comic_info, "InnerCover"
|
||||
)
|
||||
|
||||
if not im:
|
||||
for file_name in zip_file.namelist():
|
||||
for file_name in archive.namelist():
|
||||
if file_name.lower().endswith(
|
||||
(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg")
|
||||
):
|
||||
image_data = zip_file.read(file_name)
|
||||
image_data = archive.read(file_name)
|
||||
im = Image.open(BytesIO(image_data))
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -893,12 +899,12 @@ class ThumbRenderer(QObject):
|
||||
|
||||
@staticmethod
|
||||
def __cover_from_comic_info(
|
||||
zip_file: zipfile.ZipFile, comic_info: Element, cover_type: str
|
||||
archive: zipfile.ZipFile | rarfile.RarFile, comic_info: Element, cover_type: str
|
||||
) -> Image.Image | None:
|
||||
"""Extract the cover specified in ComicInfo.xml.
|
||||
|
||||
Args:
|
||||
zip_file (zipfile.ZipFile): The current ePub file.
|
||||
archive (zipfile.ZipFile | rarfile.RarFile): The current ePub file.
|
||||
comic_info (Element): The parsed ComicInfo.xml.
|
||||
cover_type (str): The type of cover to load.
|
||||
|
||||
@@ -909,10 +915,10 @@ class ThumbRenderer(QObject):
|
||||
|
||||
cover = comic_info.find(f"./*Page[@Type='{cover_type}']")
|
||||
if cover is not None:
|
||||
pages = [f for f in zip_file.namelist() if f != "ComicInfo.xml"]
|
||||
pages = [f for f in archive.namelist() if f != "ComicInfo.xml"]
|
||||
page_name = pages[int(cover.get("Image"))]
|
||||
if page_name.endswith((".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg")):
|
||||
image_data = zip_file.read(page_name)
|
||||
image_data = archive.read(page_name)
|
||||
im = Image.open(BytesIO(image_data))
|
||||
|
||||
return im
|
||||
@@ -1573,7 +1579,7 @@ class ThumbRenderer(QObject):
|
||||
if MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.EBOOK_TYPES, mime_fallback=True
|
||||
):
|
||||
image = self._epub_cover(_filepath)
|
||||
image = self._epub_cover(_filepath, ext)
|
||||
# Krita ========================================================
|
||||
elif MediaCategories.is_ext_in_category(
|
||||
ext, MediaCategories.KRITA_TYPES, mime_fallback=True
|
||||
|
||||
Reference in New Issue
Block a user