feat: render .cbr thumbnails. (#1112)

This commit is contained in:
Sola-ris
2025-09-08 21:51:01 +02:00
committed by GitHub
parent d9c7d58e89
commit 2eb9aad12d
3 changed files with 34 additions and 13 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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