feat: Enhance support for CBR format

This commit is contained in:
tssujt
2025-07-09 22:29:53 +08:00
parent 7d82752546
commit f015d998e1
4 changed files with 169 additions and 70 deletions
+56 -8
View File
@@ -1,6 +1,6 @@
# CBZ # CBZ
CBZ is a Python library designed for creating, manipulating, and viewing comic book files in CBZ format. It offers a straightforward interface to pack comic pages into CBZ archives, extract metadata, and display comics using a built-in player. CBZ is a Python library designed for creating, manipulating, and viewing comic book files in CBZ, CBR, and PDF formats. It offers a straightforward interface to pack comic pages into CBZ archives, extract metadata from CBZ and CBR files, and display comics using a built-in player.
## Features ## Features
@@ -8,8 +8,9 @@ CBZ is a Python library designed for creating, manipulating, and viewing comic b
- 📚 Pack images into CBZ format for comics and manga - 📚 Pack images into CBZ format for comics and manga
- 📝 Extract and manage title, series, format, and more - 📝 Extract and manage title, series, format, and more
- 🖼️ Handle comic pages with attributes like type and format - 🖼️ Handle comic pages with attributes like type and format
- 📦 Unpack CBZ files to retrieve comic information - 📦 Unpack CBZ and CBR files to retrieve comic information, or extract images from PDF files
- 🛠️ Built-in player for viewing CBZ comics - 🛠️ Built-in player for viewing CBZ, CBR, and PDF comics
- 📚 Full CBR (RAR) format support for reading existing archives
- ❤️ Fully Open-Source! Pull Requests Welcome - ❤️ Fully Open-Source! Pull Requests Welcome
## Installation ## Installation
@@ -73,7 +74,15 @@ if __name__ == '__main__':
## Player ## Player
CBZ includes a command-line player for viewing CBZ comic book files. Simply run cbzplayer <file> to launch the player with the specified CBZ file. CBZ includes a command-line player for viewing comic book files in multiple formats. Simply run `cbzplayer <file>` to launch the player with the specified comic book file.
### Supported Formats
- **CBZ** (Comic Book ZIP) - Standard ZIP archives containing images and metadata
- **CBR** (Comic Book RAR) - RAR archives containing images and metadata
- **PDF** - Portable Document Format files with embedded images (images only, no metadata)
### Usage
````shell ````shell
usage: cbzplayer [-h] <file> usage: cbzplayer [-h] <file>
@@ -81,13 +90,36 @@ usage: cbzplayer [-h] <file>
Launch CBZ player with a comic book file Launch CBZ player with a comic book file
positional arguments: positional arguments:
<file> Path to the CBZ comic book file. <file> Path to the CBZ, CBR, or PDF comic book file.
options: options:
-h, --help show this help message and exit -h, --help show this help message and exit
```` ````
### Examples
```shell
# View a CBZ file
cbzplayer my_comic.cbz
# View a CBR file
cbzplayer my_comic.cbr
# View a PDF file
cbzplayer my_comic.pdf
```
### Requirements for CBR Support
CBR file support requires:
- The `rarfile` Python package (automatically installed with CBZ)
- An external RAR extraction tool such as:
- `unrar` (recommended) - Available in most package managers
- `rar` - Commercial RAR archiver
- `7zip` - Free alternative with RAR support
For installation instructions and compatibility details, see the [rarfile documentation](https://github.com/markokr/rarfile).
## Detailed Usage ## Detailed Usage
### Creating a ComicInfo Object ### Creating a ComicInfo Object
@@ -135,14 +167,30 @@ Pack the comic into a CBZ file format:
cbz_content = comic.pack() cbz_content = comic.pack()
``` ```
### Unpacking from CBZ ### Loading from Different Formats
Load a comic from an existing CBZ file: Load a comic from an existing CBZ file (with metadata):
```python ```python
comic_from_cbz = ComicInfo.from_cbz('/path/to/your_comic.cbz') comic_from_cbz = ComicInfo.from_cbz('/path/to/your_comic.cbz')
``` ```
Load a comic from an existing CBR file (with metadata):
```python
comic_from_cbr = ComicInfo.from_cbr('/path/to/your_comic.cbr')
```
Load a comic from a PDF file (images only, no metadata):
```python
comic_from_pdf = ComicInfo.from_pdf('/path/to/your_comic.pdf')
```
**Notes**:
- CBR support requires an external RAR extraction tool. For detailed compatibility information and advanced configuration, see the [rarfile documentation](https://github.com/markokr/rarfile).
- PDF files only provide image content; comic metadata (title, series, etc.) is not available from PDF files.
## Contributors ## Contributors
<a href="https://github.com/hyugogirubato"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/65763543?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="hyugogirubato"/></a> <a href="https://github.com/hyugogirubato"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/65763543?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="hyugogirubato"/></a>
+8 -3
View File
@@ -8,7 +8,7 @@ from cbz.comic import ComicInfo
def main() -> None: def main() -> None:
# Parse command-line arguments # Parse command-line arguments
parser = argparse.ArgumentParser(description='Launch CBZ player with a comic book file') parser = argparse.ArgumentParser(description='Launch CBZ player with a comic book file')
parser.add_argument('comic_path', type=Path, metavar='<file>', help='Path to the CBZ comic book file.') parser.add_argument('comic_path', type=Path, metavar='<file>', help='Path to the CBZ, CBR, or PDF comic book file.')
args = parser.parse_args() args = parser.parse_args()
# Validate the provided path # Validate the provided path
@@ -17,9 +17,14 @@ def main() -> None:
print(f'Error: The file "{comic_path}" does not exist or is not a valid file.') print(f'Error: The file "{comic_path}" does not exist or is not a valid file.')
exit(1) exit(1)
# Create ComicInfo object from CBZ file # Create ComicInfo object from comic file
try: try:
comic_info = ComicInfo.from_pdf(comic_path) if comic_path.suffix == '.pdf' else ComicInfo.from_cbz(comic_path) if comic_path.suffix == '.pdf':
comic_info = ComicInfo.from_pdf(comic_path)
elif comic_path.suffix == '.cbr':
comic_info = ComicInfo.from_cbr(comic_path)
else:
comic_info = ComicInfo.from_cbz(comic_path)
# Launch the CBZ player # Launch the CBZ player
comic_info.show() comic_info.show()
except Exception as e: except Exception as e:
+68 -23
View File
@@ -8,8 +8,9 @@ from io import BytesIO
from typing import Union from typing import Union
from pathlib import Path from pathlib import Path
from pypdf import PdfReader import rarfile
import xmltodict import xmltodict
from pypdf import PdfReader
from cbz.constants import XML_NAME, COMIC_FIELDS, IMAGE_FORMAT, PAGE_FIELDS from cbz.constants import XML_NAME, COMIC_FIELDS, IMAGE_FORMAT, PAGE_FIELDS
from cbz.models import ComicModel from cbz.models import ComicModel
@@ -66,7 +67,25 @@ class ComicInfo(ComicModel):
""" """
if not isinstance(path, (Path, str)): if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}') raise ValueError(f'Expecting Path object or path string, got {path!r}')
return cls.__unpack_zip(path) return cls.__unpack_zip(Path(path))
@classmethod
def from_cbr(cls, path: Union[Path, str]) -> ComicInfo:
"""
Create a ComicInfo instance from a CBR file.
Args:
path (Union[Path, str]): Path to the CBR file.
Returns:
ComicInfo: An instance of ComicInfo.
Raises:
ValueError: If the provided path is not a Path object or a string.
"""
if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}')
return cls.__unpack_rar(Path(path))
@classmethod @classmethod
def from_pdf(cls, path: Union[Path, str]) -> ComicInfo: def from_pdf(cls, path: Union[Path, str]) -> ComicInfo:
@@ -84,7 +103,7 @@ class ComicInfo(ComicModel):
""" """
if not isinstance(path, (Path, str)): if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}') raise ValueError(f'Expecting Path object or path string, got {path!r}')
return cls.__unpack_pdf(path) return cls.__unpack_pdf(Path(path))
@staticmethod @staticmethod
def __unpack_pdf(path: Path) -> ComicInfo: def __unpack_pdf(path: Path) -> ComicInfo:
@@ -107,18 +126,7 @@ class ComicInfo(ComicModel):
return ComicInfo.from_pages(pages=pages) return ComicInfo.from_pages(pages=pages)
@staticmethod @staticmethod
def __unpack_zip(path: Path) -> ComicInfo: def __extract_info(items: dict, fields: dict) -> dict:
"""
Unpack a CBZ file and create a ComicInfo instance.
Args:
path (Path): Path to the CBZ file.
Returns:
ComicInfo: An instance of ComicInfo.
"""
def __info(items: dict, fields: dict) -> dict:
""" """
Extract and convert field information from the provided items and fields. Extract and convert field information from the provided items and fields.
@@ -135,18 +143,27 @@ class ComicInfo(ComicModel):
content[key] = field_type(items[field_key]) content[key] = field_type(items[field_key])
return content return content
pages = [] @staticmethod
def __process_archive(archive_file: zipfile.ZipFile | rarfile.RarFile) -> ComicInfo:
"""
Common logic for processing archive files (CBZ/CBR).
with zipfile.ZipFile(path, 'r', zipfile.ZIP_STORED) as zf: Args:
names = zf.namelist() archive_file: Archive file object (ZipFile or RarFile)
Returns:
ComicInfo: An instance of ComicInfo.
"""
pages = []
names = archive_file.namelist()
comic_info = {} comic_info = {}
if XML_NAME in names: if XML_NAME in names:
with zf.open(XML_NAME, 'r') as f: with archive_file.open(XML_NAME, 'r') as f:
comic_info = xmltodict.parse(f.read()).get('ComicInfo', {}) comic_info = xmltodict.parse(f.read()).get('ComicInfo', {})
names.remove(XML_NAME) names.remove(XML_NAME)
comic = __info( comic = ComicInfo.__extract_info(
items=comic_info, items=comic_info,
fields=COMIC_FIELDS fields=COMIC_FIELDS
) )
@@ -156,18 +173,46 @@ class ComicInfo(ComicModel):
suffix = Path(name).suffix suffix = Path(name).suffix
if suffix: if suffix:
assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}' assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}'
with zf.open(name, 'r') as f: with archive_file.open(name, 'r') as f:
page_info = {} page_info = {}
if i < len(pages_info): if i < len(pages_info):
page_info = __info( page_info = ComicInfo.__extract_info(
items=pages_info[i], items=pages_info[i],
fields=PAGE_FIELDS fields=PAGE_FIELDS
) )
page_info['name'] = Path(f.name).name page_info['name'] = Path(name).name
pages.append(PageInfo.loads(data=f.read(), **page_info)) pages.append(PageInfo.loads(data=f.read(), **page_info))
return ComicInfo.from_pages(pages=pages, **comic) return ComicInfo.from_pages(pages=pages, **comic)
@staticmethod
def __unpack_rar(path: Path) -> ComicInfo:
"""
Unpack a CBR file and create a ComicInfo instance.
Args:
path (Path): Path to the CBR file.
Returns:
ComicInfo: An instance of ComicInfo.
"""
with rarfile.RarFile(path, 'r') as rf:
return ComicInfo.__process_archive(rf)
@staticmethod
def __unpack_zip(path: Path) -> ComicInfo:
"""
Unpack a CBZ file and create a ComicInfo instance.
Args:
path (Path): Path to the CBZ file.
Returns:
ComicInfo: An instance of ComicInfo.
"""
with zipfile.ZipFile(path, 'r', zipfile.ZIP_STORED) as zf:
return ComicInfo.__process_archive(zf)
def get_info(self) -> dict: def get_info(self) -> dict:
""" """
Get the comic information as a dictionary. Get the comic information as a dictionary.
+5 -4
View File
@@ -5,11 +5,11 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry] [tool.poetry]
name = "cbz" name = "cbz"
version = "3.3.7" version = "3.3.7"
description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ format, offering seamless packaging, metadata handling, and built-in viewing capabilities" description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ, CBR, and PDF formats, offering seamless packaging, metadata handling for CBZ/CBR files, and built-in viewing capabilities"
license = "MIT" license = "MIT"
authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"] authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"]
readme = "README.md" readme = "README.md"
keywords = ["python", "cbz", "ebooks", "manga", "comics", "webtoons"] keywords = ["python", "cbz", "cbr", "pdf", "ebooks", "manga", "comics", "webtoons"]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
@@ -17,12 +17,12 @@ classifiers = [
"Natural Language :: English", "Natural Language :: English",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Topic :: Utilities", "Topic :: Utilities",
"Topic :: Software Development :: Libraries :: Python Modules" "Topic :: Software Development :: Libraries :: Python Modules",
] ]
include = [ include = [
{ path = "CHANGELOG.md", format = "sdist" }, { path = "CHANGELOG.md", format = "sdist" },
{ path = "README.md", format = "sdist" }, { path = "README.md", format = "sdist" },
{ path = "LICENSE", format = "sdist" } { path = "LICENSE", format = "sdist" },
] ]
[tool.poetry.urls] [tool.poetry.urls]
@@ -35,6 +35,7 @@ xmltodict = ">=0.13.0"
langcodes = ">=3.4.0" langcodes = ">=3.4.0"
Pillow = ">=10.4.0" Pillow = ">=10.4.0"
pypdf = ">=5.6.1" pypdf = ">=5.6.1"
rarfile = ">=4.0"
[tool.poetry.scripts] [tool.poetry.scripts]
cbzplayer = "cbz.__main__:main" cbzplayer = "cbz.__main__:main"