Skip to content

Commit 2eb9aad

Browse files
authored
feat: render .cbr thumbnails. (#1112)
1 parent d9c7d58 commit 2eb9aad

File tree

3 files changed

+34
-13
lines changed

3 files changed

+34
-13
lines changed

docs/install.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,20 @@ Don't forget to rebuild!
219219

220220
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.
221221

222+
### RAR extractor
223+
224+
To generate thumbnails for RAR-based files (like `.cbr`) you'll need an extractor capable of handling them.
225+
226+
On Linux you'll need to install either `unrar` (likely in you distro's non-free repository) or `unrar-free` from your package manager.
227+
228+
On Mac `unrar` can be installed through Homebrew's [`rar`](https://formulae.brew.sh/cask/rar) formula.
229+
230+
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`.
231+
232+
#### Note
233+
234+
Both `unrar` and `WinRAR` require a license, but since the evaluation copy has no time limit you can simply dismiss the prompt.
235+
222236
### ripgrep
223237

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

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies = [
2323
"pydantic~=2.10",
2424
"pydub~=0.25",
2525
"PySide6==6.8.0.*",
26+
"rarfile==4.2",
2627
"rawpy~=0.24",
2728
"Send2Trash~=1.8",
2829
"SQLAlchemy~=2.0",

src/tagstudio/qt/previews/renderer.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import cv2
2020
import numpy as np
2121
import pillow_avif # noqa: F401 # pyright: ignore[reportUnusedImport]
22+
import rarfile
2223
import rawpy
2324
import srctools
2425
import structlog
@@ -857,33 +858,38 @@ def _powerpoint_thumb(filepath: Path) -> Image.Image | None:
857858
return im
858859

859860
@staticmethod
860-
def _epub_cover(filepath: Path) -> Image.Image | None:
861+
def _epub_cover(filepath: Path, ext: str) -> Image.Image | None:
861862
"""Extracts the cover specified by ComicInfo.xml or first image found in the ePub file.
862863
863864
Args:
864865
filepath (Path): The path to the ePub file.
866+
ext (str): The file extension.
865867
866868
Returns:
867869
Image: The cover specified in ComicInfo.xml,
868870
the first image found in the ePub file, or None by default.
869871
"""
870872
im: Image.Image | None = None
871873
try:
872-
with zipfile.ZipFile(filepath, "r") as zip_file:
873-
if "ComicInfo.xml" in zip_file.namelist():
874-
comic_info = ET.fromstring(zip_file.read("ComicInfo.xml"))
875-
im = ThumbRenderer.__cover_from_comic_info(zip_file, comic_info, "FrontCover")
874+
archiver: type[zipfile.ZipFile] | type[rarfile.RarFile] = zipfile.ZipFile
875+
if ext == ".cbr":
876+
archiver = rarfile.RarFile
877+
878+
with archiver(filepath, "r") as archive:
879+
if "ComicInfo.xml" in archive.namelist():
880+
comic_info = ET.fromstring(archive.read("ComicInfo.xml"))
881+
im = ThumbRenderer.__cover_from_comic_info(archive, comic_info, "FrontCover")
876882
if not im:
877883
im = ThumbRenderer.__cover_from_comic_info(
878-
zip_file, comic_info, "InnerCover"
884+
archive, comic_info, "InnerCover"
879885
)
880886

881887
if not im:
882-
for file_name in zip_file.namelist():
888+
for file_name in archive.namelist():
883889
if file_name.lower().endswith(
884890
(".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg")
885891
):
886-
image_data = zip_file.read(file_name)
892+
image_data = archive.read(file_name)
887893
im = Image.open(BytesIO(image_data))
888894
break
889895
except Exception as e:
@@ -893,12 +899,12 @@ def _epub_cover(filepath: Path) -> Image.Image | None:
893899

894900
@staticmethod
895901
def __cover_from_comic_info(
896-
zip_file: zipfile.ZipFile, comic_info: Element, cover_type: str
902+
archive: zipfile.ZipFile | rarfile.RarFile, comic_info: Element, cover_type: str
897903
) -> Image.Image | None:
898904
"""Extract the cover specified in ComicInfo.xml.
899905
900906
Args:
901-
zip_file (zipfile.ZipFile): The current ePub file.
907+
archive (zipfile.ZipFile | rarfile.RarFile): The current ePub file.
902908
comic_info (Element): The parsed ComicInfo.xml.
903909
cover_type (str): The type of cover to load.
904910
@@ -909,10 +915,10 @@ def __cover_from_comic_info(
909915

910916
cover = comic_info.find(f"./*Page[@Type='{cover_type}']")
911917
if cover is not None:
912-
pages = [f for f in zip_file.namelist() if f != "ComicInfo.xml"]
918+
pages = [f for f in archive.namelist() if f != "ComicInfo.xml"]
913919
page_name = pages[int(cover.get("Image"))]
914920
if page_name.endswith((".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg")):
915-
image_data = zip_file.read(page_name)
921+
image_data = archive.read(page_name)
916922
im = Image.open(BytesIO(image_data))
917923

918924
return im
@@ -1573,7 +1579,7 @@ def _render(
15731579
if MediaCategories.is_ext_in_category(
15741580
ext, MediaCategories.EBOOK_TYPES, mime_fallback=True
15751581
):
1576-
image = self._epub_cover(_filepath)
1582+
image = self._epub_cover(_filepath, ext)
15771583
# Krita ========================================================
15781584
elif MediaCategories.is_ext_in_category(
15791585
ext, MediaCategories.KRITA_TYPES, mime_fallback=True

0 commit comments

Comments
 (0)