mirror of
https://github.com/hyugogirubato/cbz.git
synced 2026-09-26 21:41:03 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
871716971a | ||
|
|
597cb41b00 | ||
|
|
a489ae17fb | ||
|
|
abc69d4673 | ||
|
|
6a1111a6bb | ||
|
|
220f19fe46 | ||
|
|
bbab9ba3b3 | ||
|
|
5824a8428a | ||
|
|
23f7e37561 | ||
|
|
5cc4732238 | ||
|
|
9d5d539311 | ||
|
|
f8274ae26b | ||
|
|
c045c548de | ||
|
|
7355e5edc0 | ||
|
|
5eee972d75 | ||
|
|
3604a4e98f | ||
|
|
19c1b01ea6 |
@@ -10,10 +10,10 @@ jobs:
|
|||||||
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,33 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [3.4.5] - 2025-10-19
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- More flexible suffix checking to support complex archive paths.
|
||||||
|
- Improved validation in `PageInfo.load()` to handle empty or invalid data correctly.
|
||||||
|
|
||||||
|
## [3.4.4] - 2025-09-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added `[pillow]` extension to support `.jxl` and `.avif` images.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Files in the input archives are now sorted alphabetically.
|
||||||
|
|
||||||
|
## [3.4.3] - 2025-08-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Bump actions/checkout from 4 to 5.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix ComicInfo XML parsing for single page comic.
|
||||||
|
|
||||||
## [3.4.2] - 2025-07-26
|
## [3.4.2] - 2025-07-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -205,6 +232,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
|
|
||||||
- Initial release.
|
- Initial release.
|
||||||
|
|
||||||
|
[3.4.5]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.5
|
||||||
|
[3.4.4]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.4
|
||||||
|
[3.4.3]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.3
|
||||||
[3.4.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.2
|
[3.4.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.2
|
||||||
[3.4.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.1
|
[3.4.1]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.1
|
||||||
[3.4.0]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.0
|
[3.4.0]: https://github.com/hyugogirubato/cbz/releases/tag/v3.4.0
|
||||||
|
|||||||
@@ -216,10 +216,12 @@ This project uses the following third-party libraries:
|
|||||||
|
|
||||||
- **[langcodes](https://pypi.org/project/langcodes/)** - MIT License
|
- **[langcodes](https://pypi.org/project/langcodes/)** - MIT License
|
||||||
- **[Pillow](https://pypi.org/project/Pillow/)** - HPND License
|
- **[Pillow](https://pypi.org/project/Pillow/)** - HPND License
|
||||||
|
- **[pillow-avif-plugin](https://pypi.org/project/pillow-avif-plugin/)** - MIT License
|
||||||
|
- **[pillow-jxl-plugin](https://pypi.org/project/pillow-jxl-plugin/)** - MIT License
|
||||||
- **[pypdf](https://pypi.org/project/pypdf/)** - BSD License
|
- **[pypdf](https://pypi.org/project/pypdf/)** - BSD License
|
||||||
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
|
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
|
||||||
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
|
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
|
||||||
|
|
||||||
* * *
|
---
|
||||||
|
|
||||||
© hyugogirubato 2025
|
© hyugogirubato 2025
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
from .comic import ComicInfo
|
from .comic import ComicInfo
|
||||||
from .page import PageInfo
|
from .page import PageInfo
|
||||||
|
|
||||||
|
# Register optional Pillow plugins
|
||||||
|
for _plugin in ('pillow_avif', 'pillow_jxl'):
|
||||||
|
try:
|
||||||
|
__import__(_plugin)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|||||||
+10
-4
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -156,12 +157,12 @@ class ComicInfo(ComicModel):
|
|||||||
ComicInfo: An instance of ComicInfo.
|
ComicInfo: An instance of ComicInfo.
|
||||||
"""
|
"""
|
||||||
pages = []
|
pages = []
|
||||||
names = archive_file.namelist()
|
names = sorted(archive_file.namelist())
|
||||||
comic_info = {}
|
comic_info = {}
|
||||||
|
|
||||||
if XML_NAME in names:
|
if XML_NAME in names:
|
||||||
with archive_file.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(), force_list=('Page',)).get('ComicInfo', {})
|
||||||
names.remove(XML_NAME)
|
names.remove(XML_NAME)
|
||||||
|
|
||||||
comic = ComicInfo.__extract_info(
|
comic = ComicInfo.__extract_info(
|
||||||
@@ -172,8 +173,13 @@ class ComicInfo(ComicModel):
|
|||||||
pages_info = comic_info.get('Pages', {}).get('Page', [])
|
pages_info = comic_info.get('Pages', {}).get('Page', [])
|
||||||
for i, name in enumerate(names):
|
for i, name in enumerate(names):
|
||||||
suffix = Path(name).suffix
|
suffix = Path(name).suffix
|
||||||
if suffix:
|
|
||||||
assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}'
|
# Some archives may contain folders or hidden files (e.g., ".extra/" or "__MACOSX/.DS_Store")
|
||||||
|
# that could corrupt the suffix detection and cause invalid image handling.
|
||||||
|
if not suffix or suffix not in IMAGE_FORMAT:
|
||||||
|
logging.warning(f'Skipping unsupported or invalid file: {name!r} (suffix={suffix!r})')
|
||||||
|
continue
|
||||||
|
|
||||||
with archive_file.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):
|
||||||
|
|||||||
+8
-6
@@ -5,12 +5,14 @@ from langcodes import Language
|
|||||||
|
|
||||||
XML_NAME = 'ComicInfo.xml'
|
XML_NAME = 'ComicInfo.xml'
|
||||||
IMAGE_FORMAT = {
|
IMAGE_FORMAT = {
|
||||||
'.jpeg', '.jpg', # Joint Photographic Experts Group
|
'.jpeg', '.jpg', # JPEG (Joint Photographic Experts Group)
|
||||||
'.png', # Portable Network Graphics
|
'.png', # PNG (Portable Network Graphics)
|
||||||
'.gif', # Graphics Interchange Format
|
'.gif', # GIF (Graphics Interchange Format)
|
||||||
'.bmp', # Bitmap Image File
|
'.bmp', # BMP (Bitmap Image File)
|
||||||
'.tiff', '.tif', # Tagged Image File Format
|
'.tiff', '.tif', # TIFF (Tagged Image File Format)
|
||||||
'.webp' # Web Picture Format
|
'.webp', # WebP (Web Picture Format, by Google)
|
||||||
|
'.jxl', # JPEG XL (Next-generation JPEG format)
|
||||||
|
'.avif' # AVIF (AV1 Image File Format, based on AV1 codec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -75,6 +75,8 @@ class PageInfo(PageModel):
|
|||||||
data = base64.b64decode(data)
|
data = base64.b64decode(data)
|
||||||
if not isinstance(data, bytes):
|
if not isinstance(data, bytes):
|
||||||
raise ValueError(f'Expecting Bytes or Base64 input, got {data!r}')
|
raise ValueError(f'Expecting Bytes or Base64 input, got {data!r}')
|
||||||
|
if not data.strip():
|
||||||
|
raise ValueError(f'Empty or null data provided (length={len(data)})')
|
||||||
return cls(data, **kwargs)
|
return cls(data, **kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -98,7 +100,7 @@ class PageInfo(PageModel):
|
|||||||
path = Path(path)
|
path = Path(path)
|
||||||
with path.open(mode='rb') as f:
|
with path.open(mode='rb') as f:
|
||||||
kwargs.setdefault('name', path.name)
|
kwargs.setdefault('name', path.name)
|
||||||
return cls(f.read(), **kwargs)
|
return cls.loads(f.read(), **kwargs)
|
||||||
|
|
||||||
def show(self) -> None:
|
def show(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+7
-2
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "cbz"
|
name = "cbz"
|
||||||
version = "3.4.2"
|
version = "3.4.5"
|
||||||
description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ, CBR, and PDF formats, 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 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>"]
|
||||||
@@ -46,6 +46,12 @@ Pillow = ">=10.4.0"
|
|||||||
pypdf = ">=5.7.0"
|
pypdf = ">=5.7.0"
|
||||||
rarfile = ">=4.2"
|
rarfile = ">=4.2"
|
||||||
xmltodict = ">=0.14.2"
|
xmltodict = ">=0.14.2"
|
||||||
|
pillow-avif-plugin = { version = ">=1.5.2", optional = true, markers = "python_version >= '3.9'" }
|
||||||
|
pillow-jxl-plugin = { version = ">=1.3.4", optional = true, markers = "python_version >= '3.9'" }
|
||||||
|
|
||||||
|
|
||||||
|
[tool.poetry.extras]
|
||||||
|
pillow = ["pillow-avif-plugin", "pillow-jxl-plugin"]
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
cbzplayer = "cbz.__main__:main"
|
cbzplayer = "cbz.__main__:main"
|
||||||
@@ -55,7 +61,6 @@ name = "localpypi"
|
|||||||
url = "https://pypi.org/simple/"
|
url = "https://pypi.org/simple/"
|
||||||
priority = "primary"
|
priority = "primary"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = ">=7.4.0,<9.0.0"
|
pytest = ">=7.4.0,<9.0.0"
|
||||||
pytest-cov = ">=5.0.0"
|
pytest-cov = ">=5.0.0"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
@@ -157,3 +158,22 @@ class TestComicInfo:
|
|||||||
|
|
||||||
assert comic.title == 'Empty Test'
|
assert comic.title == 'Empty Test'
|
||||||
assert len(comic.pages) == 0
|
assert len(comic.pages) == 0
|
||||||
|
|
||||||
|
def test_single_page_comic_load(self, images_dir):
|
||||||
|
"""Test loading comic with a single page."""
|
||||||
|
image_paths = sorted(list(images_dir.iterdir()))[:1]
|
||||||
|
pages = [PageInfo.load(path=path) for path in image_paths]
|
||||||
|
|
||||||
|
comic = ComicInfo.from_pages(pages=pages, title="Single Page Test")
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
temp_path = Path(temp_dir) / "single_page.cbz"
|
||||||
|
data = comic.pack()
|
||||||
|
with open(temp_path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
assert temp_path.exists()
|
||||||
|
|
||||||
|
comic_loaded = ComicInfo.from_cbz(temp_path)
|
||||||
|
assert comic_loaded.title == "Single Page Test"
|
||||||
|
assert len(comic_loaded.pages) == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user