44 Commits
Author SHA1 Message Date
hyugogirubato c045c548de release v3.4.4 2025-08-31 21:21:53 +02:00
hyugogirubato 7355e5edc0 Merge pull request #23 from hyugogirubato/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-08-31 21:14:36 +02:00
hyugogirubato 5eee972d75 Merge pull request #22 from tssujt/single
Fix ComicInfo XML parsing for single page comic
2025-08-31 21:14:07 +02:00
dependabot[bot] 3604a4e98f Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-18 23:05:20 +00:00
tssujt 19c1b01ea6 fix ComicInfo XML parsing for single page comic 2025-08-01 14:27:05 +08:00
hyugogirubato 7e599a1adb release v3.4.2 2025-07-26 17:21:02 +02:00
hyugogirubato 312eb2da13 Merge pull request #21 from hyugogirubato/dependabot/pip/pytest-gte-7.4.0-and-lt-9.0.0
Update pytest requirement from >=7.4.0,<8.0.0 to >=7.4.0,<9.0.0
2025-07-26 17:19:39 +02:00
hyugogirubato b7d1b631b9 fixing typo 2025-07-26 17:17:49 +02:00
hyugogirubato fe43024656 format 2025-07-26 17:17:28 +02:00
hyugogirubato 58a5256dc1 moved example to test dir 2025-07-26 17:17:13 +02:00
dependabot[bot] 7d0f004345 Update pytest requirement from >=7.4.0,<8.0.0 to >=7.4.0,<9.0.0
Updates the requirements on [pytest](https://github.com/pytest-dev/pytest) to permit the latest version.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.0...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.3.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-26 14:45:37 +00:00
hyugogirubato 6aeabc2047 Merge pull request #20 from tssujt/tests
ci: Add testing framework setup
2025-07-26 16:44:30 +02:00
hyugogirubato 46b508adf8 release v3.4.0 2025-07-11 00:44:19 +02:00
hyugogirubato 6d771e3bf8 Merge pull request #19 from tssujt/typo
fix: Fix installation instructions
2025-07-11 00:34:17 +02:00
tssujt 0c4aa34f8f ci: Add testing framework setup 2025-07-10 12:08:28 +08:00
tssujt 48c99ba44f fix: Fix installation instructions 2025-07-10 10:56:23 +08:00
hyugogirubato 8af1c47e15 release v3.4.0 2025-07-09 23:42:10 +02:00
hyugogirubato 6aa6b0da21 fix markdown typo 2025-07-09 23:41:13 +02:00
hyugogirubato 526fee3617 update project date 2025-07-09 23:40:10 +02:00
hyugogirubato 6354c1d3bb add new contributor 2025-07-09 23:39:59 +02:00
hyugogirubato 3a80dfe47b add missing imports 2025-07-09 23:39:41 +02:00
hyugogirubato 58ef27f227 fix grammar 2025-07-09 23:39:28 +02:00
hyugogirubato 36b76f6d9e add missing repo reference 2025-07-09 23:39:15 +02:00
hyugogirubato 3c53f16d07 bump dep version 2025-07-09 23:39:00 +02:00
hyugogirubato 8161551ab6 specify complete py version 2025-07-09 23:38:47 +02:00
hyugogirubato 8ab1b37332 unified tk constants import 2025-07-09 23:38:24 +02:00
hyugogirubato 784f698044 update license date 2025-07-09 23:37:44 +02:00
hyugogirubato fc1e693e67 fix py 3.8 support 2025-07-09 23:37:34 +02:00
hyugogirubato 4d831f28a9 sort lib imports 2025-07-09 23:37:00 +02:00
hyugogirubato 195fca4370 apply git ignore 2025-07-09 23:00:48 +02:00
hyugogirubato 758e54fa4d Merge pull request #18 from tssujt/cbr
feat: Enhance support for CBR format
2025-07-09 22:56:53 +02:00
tssujt 7f1a0724e1 chore: Update document third-party licenses 2025-07-09 22:29:54 +08:00
tssujt f015d998e1 feat: Enhance support for CBR format 2025-07-09 22:29:53 +08:00
hyugogirubato 7d82752546 Merge pull request #17 from tssujt/pdf
Replace PyMuPDF with pypdf for PDF handling
2025-06-27 21:59:42 +02:00
tssujt e8e6eaf3ae Replace PyMuPDF with pypdf for PDF handling 2025-06-24 22:36:52 +08:00
hyugogirubato 4f777bc9bf Release v3.3.7 2025-01-04 12:51:54 +01:00
hyugogirubato c48d64cbe0 fix ctypes import for unix 2025-01-04 12:45:07 +01:00
hyugogirubato d195e25043 release v3.3.6 2024-12-22 12:05:07 +01:00
hyugogirubato 5fd7c019d1 sort imports 2024-12-22 12:04:33 +01:00
hyugogirubato c4169ebb84 more flexible dependencies 2024-12-22 12:03:39 +01:00
hyugogirubato a46bae2e2d skip poetry.lock 2024-12-22 12:00:36 +01:00
hyugogirubato 616b1eff8e fix deprecated datetime usage 2024-12-22 11:52:35 +01:00
hyugogirubato 27a313fe4c add missing name extract from zip unpack 2024-12-21 15:45:55 +01:00
hyugogirubato 72c14449fc option to skip page name rename 2024-12-21 15:37:14 +01:00
31 changed files with 1005 additions and 492 deletions
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
+27
View File
@@ -0,0 +1,27 @@
name: Tests
on: [ push, pull_request ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ]
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install poetry
poetry install --no-interaction --no-root --with dev
- name: Run tests with pytest
run: |
poetry run pytest tests/ -v --cov=cbz --cov-report=xml --cov-report=term-missing
+3 -2
View File
@@ -100,7 +100,7 @@ ipython_config.py
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
@@ -173,4 +173,5 @@ pyrightconfig.json
### CBZ ###
*.cbz
*.pdf
*.zip
*.zip
*cbr
+60
View File
@@ -4,6 +4,60 @@ 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).
## [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
### Added
- Adding CI tests.
## [3.4.1] - 2025-07-11
### Fixed
- Fix installation instructions.
## [3.4.0] - 2025-07-09
### Added
- Support for PDF files.
- Support for CBR (Comic Book RAR) files.
### Changed
- Replaced `PyMuPDF` with `pypdf` for PDF handling.
- Updated third-party license documentation for bundled libraries.
### New Contributors
- [tssujt](https://github.com/tssujt)
## [3.3.7] - 2025-01-04
### Fixed
- Import error on Unix systems for the comic player.
## [3.3.6] - 2024-12-22
### Added
- Option to not rename the name of image files loaded into pages.
### Fixed
- Removing deprecated datetime api for utcnow.
## [3.3.5] - 2024-07-22
### Fixed
@@ -161,6 +215,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Initial release.
[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.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.3.7]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.7
[3.3.6]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.6
[3.3.5]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.5
[3.3.4]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.4
[3.3.3]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.3
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2024 hyugogirubato
Copyright (c) 2025 hyugogirubato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+75 -14
View File
@@ -1,6 +1,6 @@
# 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
@@ -8,13 +8,14 @@ CBZ is a Python library designed for creating, manipulating, and viewing comic b
- 📚 Pack images into CBZ format for comics and manga
- 📝 Extract and manage title, series, format, and more
- 🖼️ Handle comic pages with attributes like type and format
- 📦 Unpack CBZ files to retrieve comic information
- 🛠️ Built-in player for viewing CBZ comics
- 📦 Unpack CBZ and CBR files to retrieve comic information, or extract images from PDF files
- 🛠️ 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
## Installation
Install KeyDive from PyPI using Poetry:
Install CBZ from PyPI using pip:
```shell
pip install cbz
@@ -73,7 +74,15 @@ if __name__ == '__main__':
## 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
usage: cbzplayer [-h] <file>
@@ -81,13 +90,37 @@ usage: cbzplayer [-h] <file>
Launch CBZ player with a comic book file
positional arguments:
<file> Path to the CBZ comic book file.
<file> Path to the CBZ, CBR, or PDF comic book file.
options:
-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
### Creating a ComicInfo Object
@@ -96,6 +129,7 @@ The `ComicInfo` class represents a comic book with metadata and pages. It suppor
```python
from cbz.comic import ComicInfo
from cbz.constants import PageType, YesNo, Manga, AgeRating, Format
from cbz.page import PageInfo
# Example usage:
@@ -135,30 +169,57 @@ Pack the comic into a CBZ file format:
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
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
<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/piskunqa"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/38443069?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="piskunqa"/></a>
<a href="https://github.com/OleskiiPyskun"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/75667382?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="OleskiiPyskun"/></a>
<a href="https://github.com/tssujt"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/17313425?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="tssujt"/></a>
<a href="https://github.com/gokender"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3709740?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="gokender"/></a>
<a href="https://github.com/domenicoblanco"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/9018104?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="domenicoblanco"/></a>
<a href="https://github.com/RivMt"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/40086827?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="RivMt"/></a>
<a href="https://github.com/flolep2607"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/24566964?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="flolep2607"/></a>
<a href="https://github.com/gokender"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/3709740?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="gokender"/></a>
## Licensing
This software is licensed under the terms of [MIT License](https://github.com/hyugogirubato/cbz/blob/main/LICENSE).
This software is licensed under the terms of [MIT License](https://github.com/hyugogirubato/cbz/blob/main/LICENSE).
You can find a copy of the license in the LICENSE file in the root folder.
* * *
### Third-Party Licenses
© hyugogirubato 2024
This project uses the following third-party libraries:
- **[langcodes](https://pypi.org/project/langcodes/)** - MIT License
- **[Pillow](https://pypi.org/project/Pillow/)** - HPND License
- **[pypdf](https://pypi.org/project/pypdf/)** - BSD License
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
---
© hyugogirubato 2025
+9 -3
View File
@@ -8,7 +8,7 @@ from cbz.comic import ComicInfo
def main() -> None:
# Parse command-line arguments
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()
# Validate the provided path
@@ -17,9 +17,15 @@ def main() -> None:
print(f'Error: The file "{comic_path}" does not exist or is not a valid file.')
exit(1)
# Create ComicInfo object from CBZ file
# Create ComicInfo object from comic file
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
comic_info.show()
except Exception as e:
+120 -70
View File
@@ -1,17 +1,18 @@
from __future__ import annotations
import zipfile
from datetime import datetime
from datetime import datetime, timezone
from enum import Enum
from io import BytesIO
from typing import Union
from typing import Union, List
from pathlib import Path
import fitz
import rarfile
import xmltodict
from pypdf import PdfReader
from cbz.constants import XML_NAME, COMIC_FIELDS, IMAGE_FORMAT, PAGE_FIELDS
from cbz.models import ComicModel
from cbz.page import PageInfo
@@ -23,27 +24,27 @@ class ComicInfo(ComicModel):
ComicInfo class that represents the comic book information and pages.
"""
def __init__(self, pages: list[PageInfo], **kwargs):
def __init__(self, pages: List[PageInfo], **kwargs):
"""
Initialize the ComicInfo instance with pages and additional attributes.
Args:
pages (list[PageInfo]): List of PageInfo objects representing the comic pages.
pages (List[PageInfo]): List of PageInfo objects representing the comic pages.
**kwargs: Additional attributes for the comic.
Attributes:
pages (list[PageInfo]): Stores the comic pages.
pages (List[PageInfo]): Stores the comic pages.
"""
super(ComicInfo, self).__init__(**kwargs)
self.pages = pages
@classmethod
def from_pages(cls, pages: list[PageInfo], **kwargs) -> ComicInfo:
def from_pages(cls, pages: List[PageInfo], **kwargs) -> ComicInfo:
"""
Create a ComicInfo instance from pages and additional attributes.
Args:
pages (list[PageInfo]): List of PageInfo objects representing the comic pages.
pages (List[PageInfo]): List of PageInfo objects representing the comic pages.
**kwargs: Additional attributes for the comic.
Returns:
@@ -67,7 +68,25 @@ class ComicInfo(ComicModel):
"""
if not isinstance(path, (Path, str)):
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
def from_pdf(cls, path: Union[Path, str]) -> ComicInfo:
@@ -85,7 +104,7 @@ class ComicInfo(ComicModel):
"""
if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}')
return cls.__unpack_pdf(path)
return cls.__unpack_pdf(Path(path))
@staticmethod
def __unpack_pdf(path: Path) -> ComicInfo:
@@ -98,18 +117,89 @@ class ComicInfo(ComicModel):
Returns:
ComicInfo: An instance of ComicInfo.
"""
pages: list[PageInfo] = []
with fitz.open(path) as pf:
for element in pf:
# Check if the page has images
images = element.get_images(full=True)
for i, image in enumerate(images):
base = pf.extract_image(image[0])
pages.append(PageInfo.loads(data=base['image']))
pages: List[PageInfo] = []
reader = PdfReader(path)
for page in reader.pages:
for image in page.images:
pages.append(PageInfo.loads(data=image.data))
assert pages, 'No valid images present in file'
return ComicInfo.from_pages(pages=pages)
@staticmethod
def __extract_info(items: dict, fields: dict) -> dict:
"""
Extract and convert field information from the provided items and fields.
Args:
items (dict): Dictionary containing item attributes.
fields (dict): Dictionary containing field mappings and types.
Returns:
dict: Dictionary with extracted and converted field information.
"""
content = {}
for key, (field_key, field_type) in fields.items():
if field_key in items:
content[key] = field_type(items[field_key])
return content
@staticmethod
def __process_archive(archive_file: Union[zipfile.ZipFile, rarfile.RarFile]) -> ComicInfo:
"""
Common logic for processing archive files (CBZ/CBR).
Args:
archive_file (Union[ZipFile, RarFile]): Archive file object (ZipFile or RarFile)
Returns:
ComicInfo: An instance of ComicInfo.
"""
pages = []
names = archive_file.namelist()
comic_info = {}
if XML_NAME in names:
with archive_file.open(XML_NAME, 'r') as f:
comic_info = xmltodict.parse(f.read(), force_list=('Page',)).get('ComicInfo', {})
names.remove(XML_NAME)
comic = ComicInfo.__extract_info(
items=comic_info,
fields=COMIC_FIELDS
)
pages_info = comic_info.get('Pages', {}).get('Page', [])
for i, name in enumerate(names):
suffix = Path(name).suffix
if suffix:
assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}'
with archive_file.open(name, 'r') as f:
page_info = {}
if i < len(pages_info):
page_info = ComicInfo.__extract_info(
items=pages_info[i],
fields=PAGE_FIELDS
)
page_info['name'] = Path(name).name
pages.append(PageInfo.loads(data=f.read(), **page_info))
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:
"""
@@ -121,55 +211,8 @@ class ComicInfo(ComicModel):
Returns:
ComicInfo: An instance of ComicInfo.
"""
def __info(items: dict, fields: dict) -> dict:
"""
Extract and convert field information from the provided items and fields.
Args:
items (dict): Dictionary containing item attributes.
fields (dict): Dictionary containing field mappings and types.
Returns:
dict: Dictionary with extracted and converted field information.
"""
content = {}
for key, (field_key, field_type) in fields.items():
if field_key in items:
content[key] = field_type(items[field_key])
return content
pages = []
with zipfile.ZipFile(path, 'r', zipfile.ZIP_STORED) as zf:
names = zf.namelist()
comic_info = {}
if XML_NAME in names:
with zf.open(XML_NAME, 'r') as f:
comic_info = xmltodict.parse(f.read()).get('ComicInfo', {})
names.remove(XML_NAME)
comic = __info(
items=comic_info,
fields=COMIC_FIELDS
)
pages_info = comic_info.get('Pages', {}).get('Page', [])
for i, name in enumerate(names):
suffix = Path(name).suffix
if suffix:
assert suffix in IMAGE_FORMAT, f'Unsupported image format: {suffix}'
with zf.open(name, 'r') as f:
page_info = {}
if i < len(pages_info):
page_info = __info(
items=pages_info[i],
fields=PAGE_FIELDS
)
pages.append(PageInfo.loads(data=f.read(), **page_info))
return ComicInfo.from_pages(pages=pages, **comic)
return ComicInfo.__process_archive(zf)
def get_info(self) -> dict:
"""
@@ -210,7 +253,7 @@ class ComicInfo(ComicModel):
comic_pages.append(dict(sorted(page_info.items())))
# https://github.com/anansi-project/rfcs/issues/3#issuecomment-671631676
utcnow = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
utcnow = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z'
comic_info.update({
'@xmlns:xsd': 'http://www.w3.org/2001/XMLSchema',
'@xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
@@ -222,10 +265,13 @@ class ComicInfo(ComicModel):
})
return comic_info
def pack(self) -> bytes:
def pack(self, rename: bool = True) -> bytes:
"""
Pack the comic information and pages into a CBZ file format.
Args:
rename (bool): Whether to rename pages to a sequential format (e.g., 'page-001.jpg').
Returns:
bytes: Bytes representing the packed CBZ file.
"""
@@ -237,7 +283,11 @@ class ComicInfo(ComicModel):
content.replace('></Page>', ' />').encode('utf-8')
)
for i, page in enumerate(self.pages):
zf.writestr(f'page-{i + 1:03d}{page.suffix}', page.content)
name = page.name
# If the page does not have a name or renaming is enabled, generate a sequential name for the page.
if not name or rename:
name = f'page-{i + 1:03d}{page.suffix}'
zf.writestr(name, page.content)
packed = zip_buffer.getvalue()
zip_buffer.close()
+1
View File
@@ -132,6 +132,7 @@ class PageModel(BaseModel):
image_width: int
image_height: int
suffix: str
name: str
__content: bytes
def __init__(self, **kwargs):
+8 -4
View File
@@ -3,9 +3,9 @@ from __future__ import annotations
import base64
from io import BytesIO
from typing import Union
from typing import Union, Optional
from pathlib import Path
from PIL import Image
from cbz.constants import IMAGE_FORMAT
@@ -17,15 +17,17 @@ class PageInfo(PageModel):
Model for representing comic book pages with additional content handling capabilities.
"""
def __init__(self, content: bytes, **kwargs):
def __init__(self, content: bytes, name: Optional[str] = None, **kwargs):
"""
Initializes a PageInfo instance.
Args:
content (bytes): The content of the page in bytes.
name (Optional[str]): The name of the page (default is None).
**kwargs: Additional keyword arguments passed to the base class initializer.
"""
super(PageInfo, self).__init__(**kwargs)
self.name = name
self.content = content
@property
@@ -93,7 +95,9 @@ class PageInfo(PageModel):
"""
if not isinstance(path, (Path, str)):
raise ValueError(f'Expecting Path object or path string, got {path!r}')
with Path(path).open(mode='rb') as f:
path = Path(path)
with path.open(mode='rb') as f:
kwargs.setdefault('name', path.name)
return cls(f.read(), **kwargs)
def show(self) -> None:
+12 -9
View File
@@ -1,13 +1,16 @@
import tkinter as tk
import os
import tkinter as tk
from io import BytesIO
from tkinter import ttk
from tkinter.constants import DISABLED, NORMAL
from ctypes import windll
from pathlib import Path
try:
from ctypes import windll
except ImportError:
windll = None # windll is not available on this platform
from PIL import Image, ImageTk
from pathlib import Path
from cbz.comic import ComicInfo
from cbz.page import PageInfo
@@ -203,7 +206,7 @@ class Player:
self.summary_text = tk.Text(self.canvas, wrap=tk.WORD, bg='white', bd=0, padx=10, pady=10)
self.summary_text.tag_configure('bold', font=('Arial', 13, 'bold'))
self.summary_text.tag_configure('normal', font=('Arial', 12), lmargin1=10, lmargin2=10)
self.summary_text.config(state=DISABLED)
self.summary_text.config(state=tk.DISABLED)
def show_page(self) -> None:
"""
@@ -219,8 +222,8 @@ class Player:
self._show_image_page()
# Update navigation button states based on current page index
self.prev_button.config(state=DISABLED if self.current_page == -1 else NORMAL)
self.next_button.config(state=DISABLED if self.current_page + 1 == len(self.comic_info.pages) else NORMAL)
self.prev_button.config(state=tk.DISABLED if self.current_page == -1 else tk.NORMAL)
self.next_button.config(state=tk.DISABLED if self.current_page + 1 == len(self.comic_info.pages) else tk.NORMAL)
# Update page index label text
self.page_index_label.config(text=f'{self.current_page + 1}/{len(self.comic_info.pages)}')
@@ -235,7 +238,7 @@ class Player:
# Display summary for the first page
infos = self.comic_info.get_info()
self.summary_text.config(state=NORMAL)
self.summary_text.config(state=tk.NORMAL)
self.summary_text.delete('1.0', tk.END)
for key, value in infos.items():
@@ -245,7 +248,7 @@ class Player:
self.summary_text.insert(tk.END, f'{key}\n', 'bold')
self.summary_text.insert(tk.END, f'{value}\n\n', 'normal')
self.summary_text.config(state=DISABLED)
self.summary_text.config(state=tk.DISABLED)
self.summary_text.place(relwidth=1, relheight=1)
def _show_image_page(self) -> None:
+2 -2
View File
@@ -1,14 +1,14 @@
from enum import Enum
from io import BytesIO
from pathlib import Path
from PIL import Image
from PIL.IcoImagePlugin import IcoFile
from pathlib import Path
def default_attr(value: any) -> any:
"""
Provides a default value based on the expected type of an attribute.
Provides a default value based on the expected type of attribute.
Args:
value (any): Expected type or class of the attribute.
Generated
-374
View File
@@ -1,374 +0,0 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "langcodes"
version = "3.4.0"
description = "Tools for labeling human languages with IETF language tags"
optional = false
python-versions = ">=3.8"
files = [
{file = "langcodes-3.4.0-py3-none-any.whl", hash = "sha256:10a4cc078b8e8937d8485d3352312a0a89a3125190db9f2bb2074250eef654e9"},
{file = "langcodes-3.4.0.tar.gz", hash = "sha256:ae5a77d1a01d0d1e91854a671890892b7ce9abb601ab7327fc5c874f899e1979"},
]
[package.dependencies]
language-data = ">=1.2"
[package.extras]
build = ["build", "twine"]
test = ["pytest", "pytest-cov"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "language-data"
version = "1.2.0"
description = "Supplementary data about languages used by the langcodes module"
optional = false
python-versions = "*"
files = [
{file = "language_data-1.2.0-py3-none-any.whl", hash = "sha256:77d5cab917f91ee0b2f1aa7018443e911cf8985ef734ca2ba3940770f6a3816b"},
{file = "language_data-1.2.0.tar.gz", hash = "sha256:82a86050bbd677bfde87d97885b17566cfe75dad3ac4f5ce44b52c28f752e773"},
]
[package.dependencies]
marisa-trie = ">=0.7.7"
[package.extras]
build = ["build", "twine"]
test = ["pytest", "pytest-cov"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "marisa-trie"
version = "1.2.0"
description = "Static memory-efficient and fast Trie-like structures for Python."
optional = false
python-versions = ">=3.7"
files = [
{file = "marisa_trie-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:61fab91fef677f0af0e818e61595f2334f7e0b3e122b24ec65889aae69ba468d"},
{file = "marisa_trie-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f5b3080316de735bd2b07265de5eea3ae176fa2fc60f9871aeaa9cdcddfc8f7"},
{file = "marisa_trie-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:77bfde3287314e91e28d3a882c7b87519ef0ee104c921df72c7819987d5e4863"},
{file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4fbb1ec1d9e891060a0aee9f9c243acec63de1e197097a14850ba38ec8a4013"},
{file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e04e9c86fe8908b61c2aebb8444217cacaed15b93d2dccaac3849e36a6dc660"},
{file = "marisa_trie-1.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a7c75a508f44e40f7af8448d466045a97534adcbb026e63989407cefb9ebfa6"},
{file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5321211647609869907e81b0230ad2dfdfa7e19fe1ee469b46304a622391e6a1"},
{file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:88660e6ee0f821872aaf63ba4b9a7513428b9cab20c69cc013c368bd72c3a4fe"},
{file = "marisa_trie-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e4535fc5458de2b59789e574cdd55923d63de5612dc159d33941af79cd62786"},
{file = "marisa_trie-1.2.0-cp310-cp310-win32.whl", hash = "sha256:bdd1d4d430e33abbe558971d1bd57da2d44ca129fa8a86924c51437dba5cb345"},
{file = "marisa_trie-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:c729e2b8f9699874b1372b5a01515b340eda1292f5e08a3fe4633b745f80ad7a"},
{file = "marisa_trie-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d62985a0e6f2cfeb36cd6afa0460063bbe83ef4bfd9afe189a99103487547210"},
{file = "marisa_trie-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1890cc993149db4aa8242973526589e8133c3f92949b0ac74c2c9a6596707ae3"},
{file = "marisa_trie-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26177cd0dadb7b44f47c17c40e16ac157c4d22ac7ed83b5a47f44713239e10d1"},
{file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3425dc81d49a374be49e3a063cb6ccdf57973201b0a30127082acea50562a85e"},
{file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:525b8df41a1a7337ed7f982eb63b704d7d75f047e30970fcfbe9cf6fc22c5991"},
{file = "marisa_trie-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c643c66bbde6a115e4ec8713c087a9fe9cb7b7c684e6af4cf448c120fa427ea4"},
{file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a83fe83e0eab9154a2dc7c556898c86584b7779ddf4214c606fce4ceff07c13"},
{file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:49701db6bb8f1ec0133abd95f0a4891cfd6f84f3bd019e343037e31a5a5b0210"},
{file = "marisa_trie-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a3f0562863deaad58c5dc3a51f706da92582bc9084189148a45f7a12fe261a51"},
{file = "marisa_trie-1.2.0-cp311-cp311-win32.whl", hash = "sha256:b08968ccad00f54f31e38516e4452fae59dd15a3fcee56aea3101ba2304680b3"},
{file = "marisa_trie-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3ef375491e7dd71a0a7e7bf288c88750942bd1ee0c379dcd6ad43e31af67d00"},
{file = "marisa_trie-1.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:39b88f126988ea83e8458259297d2b2f9391bfba8f4dc5d7a246813aae1c1def"},
{file = "marisa_trie-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ec167b006884a90d130ee30518a9aa44cb40211f702bf07031b2d7d4d1db569b"},
{file = "marisa_trie-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b855e6286faef5411386bf9d676dfb545c09f7d109f197f347c9366aeb12f07"},
{file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cd287ff323224d87c2b739cba39614aac3737c95a254e0ff70e77d9b8df226d"},
{file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8a1c0361165231f4fb915237470afc8cc4803c535f535f4fc42ca72855b124"},
{file = "marisa_trie-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3267f438d258d7d85ee3dde363c4f96c3196ca9cd9e63fe429a59543cc544b15"},
{file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c87a0c2cccce12b07bfcb70708637c0816970282d966a1531ecda1a24bd1cc8"},
{file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d3c0e38f0501951e2322f7274a39b8e2344bbd91ceaa9da439f46022570ddc9d"},
{file = "marisa_trie-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd88a338c87e6dc130b6cea7b697580c21f0c83a8a8b46671cfecbb713d3fe24"},
{file = "marisa_trie-1.2.0-cp312-cp312-win32.whl", hash = "sha256:5cea60975184f03fbcff51339df0eb44d2abe106a1693983cc64415eb87b897b"},
{file = "marisa_trie-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b04a07b99b62b9bdf3eaf1d44571a3293ce249ce8971944e780c9c709593462f"},
{file = "marisa_trie-1.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c11af35d9304de420b359741e12b885d04f11403697efcbbe8cb50f834261ebc"},
{file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2db8e74493c3bffb480c54afaa88890a39bf90063ff5b322acf64bf076e4b36e"},
{file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcc6613bc873136dc62609b66aaa27363e2bd46c03fdab62d638f7cf69d5f82"},
{file = "marisa_trie-1.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5cb731581effb3e05258f3ddc2a155475de74bb00f61eb280f991e13b48f783"},
{file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:eba1061bbeaeec4149282beab2ae163631606f119f549a10246b014e13f9047b"},
{file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:015594427360c6ad0fa94d51ee3d50fb83b0f7278996497fd2d69f877c3de9bd"},
{file = "marisa_trie-1.2.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:36d65bcbf22a70cdd0202bd8608c2feecc58bdb9e5dd9a2f5a723b651fcab287"},
{file = "marisa_trie-1.2.0-cp37-cp37m-win32.whl", hash = "sha256:bc138625b383998f5cd0cbf6cd38d66d414f3786ae6d7b4e4a6fc970140ef4e9"},
{file = "marisa_trie-1.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:27d270a64eb655754dfb4e352c60a084b16ab999b3a97a0cdc7dbecbca3c0e35"},
{file = "marisa_trie-1.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fa1fa7f67d317a921315a65e266b9e156ce5a956076ec2b6dbf72d67c7df8216"},
{file = "marisa_trie-1.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dccef41d4af11a03558c1d101de58bd723b3039a5bc4e064250008c118037ec"},
{file = "marisa_trie-1.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:873efd212dfef2b736ff2ff43e10b348c428d5dbac7b8cb8aa777004bc8c7b0e"},
{file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8af7a21ac2ba6dc23e4257fc3a40b3070e776275d3d0b5b2ef44473ad92caf3a"},
{file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7202ba0ca1db5245feaebbeb3d0c776b2da1fffb0abc3500dd505f679686aa1"},
{file = "marisa_trie-1.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d90be28c083323909d23ff8e9b4a2764b9e75520d1bae1a277e9fa7ca20d15"},
{file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40e2a374026492ac84232897f1f1d8f92a4a1f8bcf3f0ded1f2b8b708d1acfff"},
{file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7c6e6506bd24a5799b9b4b9cf1e8d6fa281f136396ba018a95d95d4d74715227"},
{file = "marisa_trie-1.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:437bf6c0d7ba4cf17656a0e3bdd0b3c2c92c01fedfa670904177eef3116a4f45"},
{file = "marisa_trie-1.2.0-cp38-cp38-win32.whl", hash = "sha256:6aeef7b364fb3b34dbba1cc57b79f1668fad0c3f039738d65a5b0d5ddce15f47"},
{file = "marisa_trie-1.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:02f773e85cc566a24c0e0e28c744052db7691c4f13d02e4257bc657a49b9ab14"},
{file = "marisa_trie-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6ff705cb3b907bdeacb8c4b3bf0541691f52b101014d189a707ca41ebfacad59"},
{file = "marisa_trie-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006419c59866979188906babc42ae0918081c18cabc2bdabca027f68c081c127"},
{file = "marisa_trie-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7196691681ecb8a12629fb6277c33bafdb27cf2b6c18c28bc48fa42a15eab8f"},
{file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaf052c0a1f4531ee12fd4c637212e77ad2af8c3b38a0d3096622abd01a22212"},
{file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb95f3ab95ba933f6a2fa2629185e9deb9da45ff2aa4ba8cc8f722528c038ef"},
{file = "marisa_trie-1.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7459b1e1937e33daed65a6d55f8b95f9a8601f4f8749d01641cf548ecac03840"},
{file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:902ea948677421093651ca98df62d255383f865f7c353f956ef666e92500e79f"},
{file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fdf7a2d066907816726f3bf241b8cb05b698d6ffaa3c5ea2658d4ba69e87ec57"},
{file = "marisa_trie-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3540bb85b38dfc17060263e061c95a0a435681b04543d1ae7e8d7441a9790593"},
{file = "marisa_trie-1.2.0-cp39-cp39-win32.whl", hash = "sha256:fe1394e1f262e5b45d22d30bd1ef75174d1f2772e86716b5f93f9c29dfc1a779"},
{file = "marisa_trie-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:84c44cb13803723f0f76aa2ba1a657f762a0bb9d8a9b80dfff249bb1c3218dd6"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:035c4c8f3b313b4d7b7451ddd539da811a11077a9e359c6a0345f816b1bdccb3"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d4f05c2ee218a5ab09d269b640d06f9708b0cf37c842344cbdffb0661c74c472"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92ac63e1519598de946c7d9346df3bb52ed96968eb3021b4e89b51d79bc72a86"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:045f32eaeb5dcdb5beadb571ba616d7a34141764b616eebb4decce71b366f5fa"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb60c2f9897ce2bfc31a69ac25a040de4f8643ab2a339bb0ff1185e1a9dedaf8"},
{file = "marisa_trie-1.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f19c5fcf23c02f1303deb69c67603ee37ed8f01de2d8b19f1716a6cf5afd5455"},
{file = "marisa_trie-1.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a06a77075240eb83a47b780902322e66c968a06a2b6318cab06757c65ea64190"},
{file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:125016400449e46ec0e5fabd14c8314959c4dfa02ffc2861195c99efa2b5b011"},
{file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c57647dd9f9ba16fc5bb4679c915d7d48d5c0b25134fb10f095ccd839686a027"},
{file = "marisa_trie-1.2.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6601e74338fb31e1b20674257706150113463182a01d3a1310df6b8840720b17"},
{file = "marisa_trie-1.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ce2f68e1000c4c72820c5b2c9d037f326fcf75f036453a5e629f225f99b92cfc"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:069ac10a133d96b3f3ed1cc071b973a3f28490345e7941c778a1d81cf176f04a"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:de9911480ce2a0513582cb84ee4484e5ee8791e692276c7f5cd7378e114d1988"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfec001cf233e8853a29e1c2bb74031c217aa61e7bd19389007e04861855731"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd1f3ef8de89684fbdd6aaead09d53b82e718bad4375d2beb938cbd24b48c51a"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f5d8c1ecc85283b5b03a1475a5da723b94b3beda752c895b2f748477d8f1b1"},
{file = "marisa_trie-1.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:2e7540f844c1de493a90ad7d0f5bffc6a2cba19fe312d6db7b97aceff11d97f8"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2fb9243f66563285677079c9dccc697d35985287bacb36c8e685305687b0e025"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:58e2b84cbb6394f9c567f1f4351fc2995a094e1b684da9b577d4139b145401d6"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b4a8d3ed1f1b8f551b52e11a1265eaf0718f06bb206654b2c529cecda0913dd"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97652c5fbc92f52100afe1c4583625015611000fa81606ad17f1b3bbb9f3bfa"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7183d84da20c89b2a366bf581f0d79d1e248909678f164e8536f291120432e8"},
{file = "marisa_trie-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c7f4df4163202b0aa5dad3eeddf088ecb61e9101986c8b31f1e052ebd6df9292"},
{file = "marisa_trie-1.2.0.tar.gz", hash = "sha256:fedfc67497f8aa2757756b5cf493759f245d321fb78914ce125b6d75daa89b5f"},
]
[package.dependencies]
setuptools = "*"
[package.extras]
test = ["hypothesis", "pytest", "readme-renderer"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "pillow"
version = "10.4.0"
description = "Python Imaging Library (Fork)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
{file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
{file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
{file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
{file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
{file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
{file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
{file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
{file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
{file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
{file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
{file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
{file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
{file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
{file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
{file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
{file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
{file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
{file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
{file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
{file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
{file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
{file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
{file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
{file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
{file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
{file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
{file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
{file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
{file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
{file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
{file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
{file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
{file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
{file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
{file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
{file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
{file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
{file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
{file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
{file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
{file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
{file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
{file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
]
[package.extras]
docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
fpx = ["olefile"]
mic = ["olefile"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
typing = ["typing-extensions"]
xmp = ["defusedxml"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "pymupdf"
version = "1.24.7"
description = "A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents."
optional = false
python-versions = ">=3.8"
files = [
{file = "PyMuPDF-1.24.7-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:9b6984d57e127e016231b2e89b247f2b0fef07af84d4cc91e4893c6d3fde52c7"},
{file = "PyMuPDF-1.24.7-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:7b8405aac8011b445ceb1bc2d4f4c44b2c969024b301f1178399e0084541d747"},
{file = "PyMuPDF-1.24.7-cp310-none-manylinux2014_aarch64.whl", hash = "sha256:ada392c739f43608611d430305c92d1b8922f66a07bc36a4ea0c7f3f9c1b5dd2"},
{file = "PyMuPDF-1.24.7-cp310-none-manylinux2014_x86_64.whl", hash = "sha256:7a59e24873e6d135f9c07be9f47d98502171210957819c8ffa9a7cca1cac1fb8"},
{file = "PyMuPDF-1.24.7-cp310-none-musllinux_1_2_x86_64.whl", hash = "sha256:bdd9a1b703e3fedd9836d54a13b89ddf772a6eaea3b1e34dcd682b8f0b5b7123"},
{file = "PyMuPDF-1.24.7-cp310-none-win32.whl", hash = "sha256:3a2e8af6e2ef437c4e599c7e520299ea10ef02680be0a8533f78d6ee489c5a60"},
{file = "PyMuPDF-1.24.7-cp310-none-win_amd64.whl", hash = "sha256:8293773e973cf07f5d6398699d5d98151a025c9db04a333fd55a87c8f8c3c74b"},
{file = "PyMuPDF-1.24.7-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:11770619a1d5b90f5f81cc22c11967b2f473310fc9a8f2aa96c1aabff5971c63"},
{file = "PyMuPDF-1.24.7-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:5e9b6018b3af45c6ca04adb0441369eefc0d66e5023f8d54befde9bb4f078c5a"},
{file = "PyMuPDF-1.24.7-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:40743cbfa40a35e873d4b8642709f45849b3315292957d836be647cdd74d1107"},
{file = "PyMuPDF-1.24.7-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:44f4c58040c496fcfc0719df17dab60348c7988d874c5e17233fa664dcf0a984"},
{file = "PyMuPDF-1.24.7-cp311-none-musllinux_1_2_x86_64.whl", hash = "sha256:0d7e8d2155bdc6e61242bdbf5dff2f6d0118f1a3f9e5d83db334a5cf71c90c5f"},
{file = "PyMuPDF-1.24.7-cp311-none-win32.whl", hash = "sha256:3ea0bb27512e72225822d98b6747b51b02de84cc1e17a5579d90ad957760de03"},
{file = "PyMuPDF-1.24.7-cp311-none-win_amd64.whl", hash = "sha256:785c8a40fd5d76b47759f4ade6db0b78a799bb1fb2dd61bbe59784538299ee21"},
{file = "PyMuPDF-1.24.7-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:999f40a6ead5aaad0c9e23b3c2ed009080bf51be9485ae122ff2cedd6487874b"},
{file = "PyMuPDF-1.24.7-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:03facd26907dcbba63429e8b56fdbabd717f22fb6d24b38bdefc591b309ce437"},
{file = "PyMuPDF-1.24.7-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:7ec6da974092de0f842f9ef0e355c58a5529c2d5ec792709d2c6fec5ffcd80b1"},
{file = "PyMuPDF-1.24.7-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:89ccd758b5b7d1e05d615e85fb5d358ff1994ff70aaa5df4f0724bd414847949"},
{file = "PyMuPDF-1.24.7-cp312-none-musllinux_1_2_x86_64.whl", hash = "sha256:8873253ffedb7bcc8dcafa9bfe8f1efa39ac91732ccc579e1c1746eeff4ed83a"},
{file = "PyMuPDF-1.24.7-cp312-none-win32.whl", hash = "sha256:5341440bfc6c89f025990d181896b74097f6b145f5e563e3d5e5fa944c79aae6"},
{file = "PyMuPDF-1.24.7-cp312-none-win_amd64.whl", hash = "sha256:a906b57dae8f3870663f3d3a1d4a66b67cc8f5cf644bcbdd175a391f5f74f2ef"},
{file = "PyMuPDF-1.24.7-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:b18630fead76175059b973230cc175c4ed30e9f4ea03d25963d2c74440bdb78b"},
{file = "PyMuPDF-1.24.7-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:a94d556b7ea25e6195c46dc2c4114d30e8e51233a49f0ed589f8e0190d2cc3b5"},
{file = "PyMuPDF-1.24.7-cp38-none-manylinux2014_aarch64.whl", hash = "sha256:da82dc784e52b3635110995a75189cdef71b02d9a3a2635cd1d216133d547519"},
{file = "PyMuPDF-1.24.7-cp38-none-manylinux2014_x86_64.whl", hash = "sha256:d95c0cc4768ce09d06c31280cda6fc7619026e615d33beee2dbd1c74ea3a1e08"},
{file = "PyMuPDF-1.24.7-cp38-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c7c964e4b3dfbcd7a0ec11f44090002967c9652cf1dbaf5e09de333c479eae3"},
{file = "PyMuPDF-1.24.7-cp38-none-win32.whl", hash = "sha256:5812645087eaf537efb168df1a78b40b76d6b01c694b890d31e362fd007926fe"},
{file = "PyMuPDF-1.24.7-cp38-none-win_amd64.whl", hash = "sha256:b11065d5192bdc6e80046505ed68447276d3698ec1d0a3eea8dc2533f972f8fb"},
{file = "PyMuPDF-1.24.7-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:102d3427fcc3a47084eb2faa670de8a58e5d2061b4e61365b323d12a7bac0afd"},
{file = "PyMuPDF-1.24.7-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:5580c975a166a3fb1908fb055c0427bcd23abe7a7d3113777d15c0e5f8a2133b"},
{file = "PyMuPDF-1.24.7-cp39-none-manylinux2014_aarch64.whl", hash = "sha256:1553df16e6e141314fa33534800aee2bc4eab1daa8057bf791584b318aa114f6"},
{file = "PyMuPDF-1.24.7-cp39-none-manylinux2014_x86_64.whl", hash = "sha256:53cedffedc7ec5c019f8b06d0e18b1f410f4af15e0c122330b79d5e617739042"},
{file = "PyMuPDF-1.24.7-cp39-none-musllinux_1_2_x86_64.whl", hash = "sha256:371b983ba2b396a636518e2cfaa76ea8cccd38c9bdbc942256df678e421f7063"},
{file = "PyMuPDF-1.24.7-cp39-none-win32.whl", hash = "sha256:0a93d0f5c5b5cdc2f1718dbdf4b1c34aafc68ee4e44688b58c0a2894277ca941"},
{file = "PyMuPDF-1.24.7-cp39-none-win_amd64.whl", hash = "sha256:f82db6fd757fb3698075e2e68e4f6df7ae89b82a96ae102e178e5b01a250359b"},
{file = "PyMuPDF-1.24.7.tar.gz", hash = "sha256:a34ceae204f215bad51f49dd43987116c6a6269fc03d8770224f7067013b59b8"},
]
[package.dependencies]
PyMuPDFb = "1.24.6"
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "pymupdfb"
version = "1.24.6"
description = "MuPDF shared libraries for PyMuPDF."
optional = false
python-versions = ">=3.8"
files = [
{file = "PyMuPDFb-1.24.6-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:21e3ed890f736def68b9a031122ae1fb854d5cb9a53aa144b6e2ca3092416a6b"},
{file = "PyMuPDFb-1.24.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8704d2dfadc9448ce184597d8b0f9c30143e379ac948a517f9c4db7c0c71ed51"},
{file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01662584d5cfa7a91f77585f13fc23a12291cfd76a57e0a28dd5a56bf521cb2c"},
{file = "PyMuPDFb-1.24.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1f7657353529ae3f88575c83ee49eac9adea311a034b9c97248a65cee7df0e5"},
{file = "PyMuPDFb-1.24.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cebc2cedb870d1e1168e2f502eb06f05938f6df69103b0853a2b329611ec19a7"},
{file = "PyMuPDFb-1.24.6-py3-none-win32.whl", hash = "sha256:ac4b865cd1e239db04674f85e02844a0e405f8255ee7a74dfee0d86aad0d3576"},
{file = "PyMuPDFb-1.24.6-py3-none-win_amd64.whl", hash = "sha256:9224e088a0d3c188dea03831807789e245b812fbd071c8d498da8f7cc33142b2"},
{file = "PyMuPDFb-1.24.6.tar.gz", hash = "sha256:f5a40b1732d65a1e519916d698858b9ce7473e23edf9001ddd085c5293d59d30"},
]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "setuptools"
version = "71.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"},
{file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"},
]
[package.extras]
core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[[package]]
name = "xmltodict"
version = "0.13.0"
description = "Makes working with XML feel like you are working with JSON"
optional = false
python-versions = ">=3.4"
files = [
{file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"},
{file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"},
]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "localpypi"
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "080fe35089770ecee25895cdd4ee4aa14b9e9c818c199908f066bc94c4470530"
+32 -11
View File
@@ -4,12 +4,22 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "cbz"
version = "3.3.5"
description = "CBZ simplifies creating, managing, and viewing comic book files in CBZ format, offering seamless packaging, metadata handling, and built-in viewing capabilities"
version = "3.4.3"
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"
authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"]
readme = "README.md"
keywords = ["python", "cbz", "ebooks", "manga", "comics", "webtoons"]
repository = "https://github.com/hyugogirubato/cbz"
keywords = [
"python",
"cbz",
"cbr",
"pdf",
"ebooks",
"manga",
"comics",
"webtoons",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
@@ -17,12 +27,12 @@ classifiers = [
"Natural Language :: English",
"Operating System :: OS Independent",
"Topic :: Utilities",
"Topic :: Software Development :: Libraries :: Python Modules"
"Topic :: Software Development :: Libraries :: Python Modules",
]
include = [
{ path = "CHANGELOG.md", format = "sdist" },
{ path = "README.md", format = "sdist" },
{ path = "LICENSE", format = "sdist" }
{ path = "LICENSE", format = "sdist" },
]
[tool.poetry.urls]
@@ -30,11 +40,12 @@ include = [
"Changelog" = "https://github.com/hyugogirubato/cbz/blob/main/CHANGELOG.md"
[tool.poetry.dependencies]
python = "^3.8"
xmltodict = "^0.13.0"
langcodes = "^3.4.0"
Pillow = "^10.4.0"
PyMuPDF = "^1.24.7"
python = ">=3.8,<4.0"
langcodes = ">=3.4.0"
Pillow = ">=10.4.0"
pypdf = ">=5.7.0"
rarfile = ">=4.2"
xmltodict = ">=0.14.2"
[tool.poetry.scripts]
cbzplayer = "cbz.__main__:main"
@@ -44,5 +55,15 @@ name = "localpypi"
url = "https://pypi.org/simple/"
priority = "primary"
[tool.poetry.group.dev.dependencies]
pytest = ">=7.4.0,<9.0.0"
pytest-cov = ">=5.0.0"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = [
"--cov=cbz",
]
[certificates]
localpypi = { cert = false }
localpypi = { cert = false }
View File
+55
View File
@@ -0,0 +1,55 @@
from pathlib import Path
import pytest
from cbz.comic import ComicInfo
from cbz.page import PageInfo
from cbz.constants import PageType
@pytest.fixture
def fixtures_dir() -> Path:
"""Fixture that provides the path to the test fixtures directory."""
return Path(__file__).parent / 'fixtures'
@pytest.fixture
def images_dir(fixtures_dir: Path) -> Path:
"""Fixture that provides the path to the test images directory."""
return fixtures_dir / 'images'
@pytest.fixture
def sample_image_path(images_dir: Path) -> Path:
"""Fixture that provides a sample image path."""
return images_dir / 'page-000.jpg'
@pytest.fixture
def sample_cbz_file(tmp_path: Path, images_dir: Path) -> Path:
"""Fixture that creates a sample CBZ file for testing."""
# Load sample pages
image_paths = sorted(list(images_dir.iterdir()))[:3] # Use first 3 images
pages = []
for i, path in enumerate(image_paths):
page_type = PageType.FRONT_COVER if i == 0 else PageType.STORY
page = PageInfo.load(path=path, type=page_type)
pages.append(page)
# Create comic from pages
comic = ComicInfo.from_pages(
pages=pages,
title='Test Comic',
series='Test Series',
number=1,
volume=1,
year=2024
)
# Save to temporary file
cbz_path = tmp_path / 'test_comic.cbz'
cbz_content = comic.pack()
cbz_path.write_bytes(cbz_content)
return cbz_path
+2 -2
View File
@@ -7,7 +7,7 @@ from cbz.page import PageInfo
PARENT = Path(__file__).parent
if __name__ == '__main__':
paths = list((PARENT / 'images').iterdir())
paths = list((PARENT / 'fixtures' / 'images').iterdir())
# Load each page from the 'images' folder into a list of PageInfo objects
pages = [
@@ -50,7 +50,7 @@ if __name__ == '__main__':
comic.show()
# Pack the comic book content into a CBZ file format
cbz_content = comic.pack()
cbz_content = comic.pack(rename=True)
# Define the path where the CBZ file will be saved
cbz_path = PARENT / f'{comic.title}.cbz'

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 247 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Before

Width:  |  Height:  |  Size: 619 KiB

After

Width:  |  Height:  |  Size: 619 KiB

Before

Width:  |  Height:  |  Size: 592 KiB

After

Width:  |  Height:  |  Size: 592 KiB

Before

Width:  |  Height:  |  Size: 680 KiB

After

Width:  |  Height:  |  Size: 680 KiB

Before

Width:  |  Height:  |  Size: 590 KiB

After

Width:  |  Height:  |  Size: 590 KiB

Before

Width:  |  Height:  |  Size: 492 KiB

After

Width:  |  Height:  |  Size: 492 KiB

Before

Width:  |  Height:  |  Size: 479 KiB

After

Width:  |  Height:  |  Size: 479 KiB

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 549 KiB

+179
View File
@@ -0,0 +1,179 @@
import tempfile
from pathlib import Path
from typing import List
from cbz.comic import ComicInfo
from cbz.page import PageInfo
from cbz.constants import PageType, YesNo, Manga, AgeRating, Format
class TestComicInfo:
"""Test cases for ComicInfo class."""
def test_from_pages_creation(self, images_dir: Path) -> None:
"""Test creating ComicInfo from pages."""
# Load sample pages
image_paths = sorted(list(images_dir.iterdir()))[:3] # Use first 3 images
pages: List[PageInfo] = []
for i, path in enumerate(image_paths):
page_type = PageType.FRONT_COVER if i == 0 else PageType.STORY
page = PageInfo.load(path=path, type=page_type)
pages.append(page)
# Create comic from pages
comic = ComicInfo.from_pages(
pages=pages,
title='Test Comic',
series='Test Series',
number=1,
volume=1,
year=2024
)
assert comic.title == 'Test Comic'
assert comic.series == 'Test Series'
assert comic.number == 1
assert comic.volume == 1
assert comic.year == 2024
assert len(comic.pages) == 3
assert comic.pages[0].type == PageType.FRONT_COVER
assert comic.pages[1].type == PageType.STORY
def test_from_cbz_file(self, sample_cbz_file: Path) -> None:
"""Test loading ComicInfo from CBZ file."""
comic = ComicInfo.from_cbz(sample_cbz_file)
assert comic is not None
assert hasattr(comic, 'pages')
assert len(comic.pages) > 0
assert all(isinstance(page, PageInfo) for page in comic.pages)
def test_pack_cbz(self, images_dir: Path) -> None:
"""Test packing comic into CBZ format."""
# Create a simple comic
image_paths = sorted(list(images_dir.iterdir()))[:2]
pages = [PageInfo.load(path=path) for path in image_paths]
comic = ComicInfo.from_pages(
pages=pages,
title='Pack Test',
series='Test Series'
)
# Pack to CBZ
cbz_content = comic.pack()
assert isinstance(cbz_content, bytes)
assert len(cbz_content) > 0
def test_pack_with_rename(self, images_dir: Path) -> None:
"""Test packing comic with page renaming."""
image_paths = sorted(list(images_dir.iterdir()))[:2]
pages = [PageInfo.load(path=path) for path in image_paths]
comic = ComicInfo.from_pages(
pages=pages,
title='Rename Test'
)
# Pack with rename option
cbz_content = comic.pack(rename=True)
assert isinstance(cbz_content, bytes)
assert len(cbz_content) > 0
def test_comic_metadata_properties(self, images_dir: Path) -> None:
"""Test comic metadata properties."""
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='Metadata Test',
series='Test Series',
number=5,
count=10,
volume=2,
summary='Test summary',
year=2023,
month=6,
day=15,
writer='Test Writer',
penciller='Test Penciller',
inker='Test Inker',
colorist='Test Colorist',
letterer='Test Letterer',
cover_artist='Test Cover Artist',
editor='Test Editor',
publisher='Test Publisher',
imprint='Test Imprint',
genre='Test Genre',
language_iso='en',
format=Format.SERIES,
black_white=YesNo.NO,
manga=Manga.RIGHT_LEFT,
age_rating=AgeRating.TEEN,
community_rating=4
)
assert comic.title == 'Metadata Test'
assert comic.series == 'Test Series'
assert comic.number == 5
assert comic.count == 10
assert comic.volume == 2
assert comic.summary == 'Test summary'
assert comic.year == 2023
assert comic.month == 6
assert comic.day == 15
assert comic.writer == 'Test Writer'
assert comic.penciller == 'Test Penciller'
assert comic.inker == 'Test Inker'
assert comic.colorist == 'Test Colorist'
assert comic.letterer == 'Test Letterer'
assert comic.cover_artist == 'Test Cover Artist'
assert comic.editor == 'Test Editor'
assert comic.publisher == 'Test Publisher'
assert comic.imprint == 'Test Imprint'
assert comic.genre == 'Test Genre'
assert comic.language_iso == 'en'
assert comic.format == Format.SERIES
assert comic.black_white == YesNo.NO
assert comic.manga == Manga.RIGHT_LEFT
assert comic.age_rating == AgeRating.TEEN
assert comic.community_rating == 4
def test_page_count_property(self, images_dir: Path) -> None:
"""Test that page count returns correct count."""
image_paths = sorted(list(images_dir.iterdir()))[:4]
pages = [PageInfo.load(path=path) for path in image_paths]
comic = ComicInfo.from_pages(pages=pages, title='Count Test')
assert len(comic.pages) == 4
def test_empty_pages_list(self) -> None:
"""Test creating comic with empty pages list."""
comic = ComicInfo.from_pages(pages=[], title='Empty Test')
assert comic.title == 'Empty Test'
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
+265
View File
@@ -0,0 +1,265 @@
from cbz.models import BaseModel, ComicModel, PageModel
from cbz.constants import Format, YesNo, Manga, AgeRating, PageType
class TestBaseModel:
"""Test cases for BaseModel class."""
def test_base_model_creation(self) -> None:
"""Test creating BaseModel with fields."""
test_fields = {
'test_str': ('Test String', str),
'test_int': ('Test Integer', int),
'test_bool': ('Test Boolean', bool)
}
model = BaseModel(fields=test_fields)
# Check default values are set
assert hasattr(model, 'test_str')
assert hasattr(model, 'test_int')
assert hasattr(model, 'test_bool')
def test_base_model_with_kwargs(self) -> None:
"""Test creating BaseModel with keyword arguments."""
test_fields = {
'title': ('Title', str),
'number': ('Number', int),
'published': ('Published', bool)
}
model = BaseModel(
fields=test_fields,
title='Test Title',
number=42,
published=True
)
assert model.title == 'Test Title'
assert model.number == 42
assert model.published
def test_attribute_type_verification(self) -> None:
"""Test that attribute types are verified on assignment."""
test_fields = {
'count': ('Count', int),
'name': ('Name', str)
}
model = BaseModel(fields=test_fields)
# Valid assignments
model.count = 10
model.name = 'Test'
assert model.count == 10
assert model.name == 'Test'
def test_repr_method(self) -> None:
"""Test string representation of BaseModel."""
test_fields = {
'title': ('Title', str)
}
model = BaseModel(fields=test_fields, title='Test')
repr_str = repr(model)
assert isinstance(repr_str, str)
assert 'BaseModel' in repr_str
class TestComicModel:
"""Test cases for ComicModel class."""
def test_comic_model_creation(self) -> None:
"""Test creating ComicModel with default values."""
model = ComicModel()
# Check that comic-specific attributes exist
assert hasattr(model, 'title')
assert hasattr(model, 'series')
assert hasattr(model, 'number')
assert hasattr(model, 'volume')
assert hasattr(model, 'year')
assert hasattr(model, 'month')
assert hasattr(model, 'day')
def test_comic_model_with_values(self) -> None:
"""Test creating ComicModel with specific values."""
model = ComicModel(
title='Test Comic',
series='Test Series',
number=1,
volume=1,
year=2024,
month=6,
day=15,
writer='Test Writer',
publisher='Test Publisher',
language_iso='en',
format=Format.SERIES,
black_white=YesNo.NO,
manga=Manga.RIGHT_LEFT,
age_rating=AgeRating.EVERYONE
)
assert model.title == 'Test Comic'
assert model.series == 'Test Series'
assert model.number == 1
assert model.volume == 1
assert model.year == 2024
assert model.month == 6
assert model.day == 15
assert model.writer == 'Test Writer'
assert model.publisher == 'Test Publisher'
assert model.language_iso == 'en'
assert model.format == Format.SERIES
assert model.black_white == YesNo.NO
assert model.manga == Manga.RIGHT_LEFT
assert model.age_rating == AgeRating.EVERYONE
def test_comic_model_enum_properties(self) -> None:
"""Test that enum properties work correctly."""
model = ComicModel()
# Test format enum
model.format = Format.PREVIEW
assert model.format == Format.PREVIEW
# Test yes/no enum
model.black_white = YesNo.YES
assert model.black_white == YesNo.YES
# Test manga enum
model.manga = Manga.RIGHT_LEFT
assert model.manga == Manga.RIGHT_LEFT
# Test age rating enum
model.age_rating = AgeRating.TEEN
assert model.age_rating == AgeRating.TEEN
def test_comic_model_metadata_fields(self) -> None:
"""Test comic metadata fields."""
model = ComicModel(
summary='Test summary',
notes='Test notes',
genre='Adventure',
web='http://example.com',
ean='1234567890123',
community_rating=5,
main_character_or_team='Hero',
characters='Hero, Villain',
teams='Justice League',
locations='Metropolis',
scan_information='Scanned by Test',
story_arc='Origin Story',
series_group='DC Comics',
alternate_series='Alternate Universe',
alternate_number=2,
alternate_count=10
)
assert model.summary == 'Test summary'
assert model.notes == 'Test notes'
assert model.genre == 'Adventure'
assert model.web == 'http://example.com'
assert model.ean == '1234567890123'
assert model.community_rating == 5
assert model.main_character_or_team == 'Hero'
assert model.characters == 'Hero, Villain'
assert model.teams == 'Justice League'
assert model.locations == 'Metropolis'
assert model.scan_information == 'Scanned by Test'
assert model.story_arc == 'Origin Story'
assert model.series_group == 'DC Comics'
assert model.alternate_series == 'Alternate Universe'
assert model.alternate_number == 2
assert model.alternate_count == 10
class TestPageModel:
"""Test cases for PageModel class."""
def test_page_model_creation(self) -> None:
"""Test creating PageModel with default values."""
model = PageModel()
# Check that page-specific attributes exist
assert hasattr(model, 'image')
assert hasattr(model, 'type')
assert hasattr(model, 'double')
assert hasattr(model, 'image_size')
assert hasattr(model, 'key')
assert hasattr(model, 'bookmark')
assert hasattr(model, 'image_width')
assert hasattr(model, 'image_height')
assert hasattr(model, 'image_size')
# Note: format is not a base field in PageModel
def test_page_model_with_values(self) -> None:
"""Test creating PageModel with specific values."""
model = PageModel(
type=PageType.FRONT_COVER,
double=True,
image_size=1024000,
key='cover',
bookmark='Chapter 1',
image_width=800,
image_height=1200,
)
assert model.type == PageType.FRONT_COVER
assert model.double
assert model.image_size == 1024000
assert model.key == 'cover'
assert model.bookmark == 'Chapter 1'
assert model.image_width == 800
assert model.image_height == 1200
def test_page_model_page_types(self) -> None:
"""Test different page types."""
page_types = [
PageType.FRONT_COVER,
PageType.INNER_COVER,
PageType.ROUNDUP,
PageType.STORY,
PageType.ADVERTISEMENT,
PageType.EDITORIAL,
PageType.LETTERS,
PageType.PREVIEW,
PageType.BACK_COVER,
PageType.OTHER,
PageType.DELETED
]
for page_type in page_types:
model = PageModel(type=page_type)
assert model.type == page_type
def test_page_model_boolean_properties(self) -> None:
"""Test boolean properties in PageModel."""
model = PageModel()
# Test double property
model.double = True
assert model.double
model.double = False
assert not model.double
def test_page_model_numeric_properties(self) -> None:
"""Test numeric properties in PageModel."""
model = PageModel(
image_size=2048000,
image_width=1920,
image_height=1080
)
assert model.image_size == 2048000
assert model.image_width == 1920
assert model.image_height == 1080
# Test that they're integers
assert isinstance(model.image_size, int)
assert isinstance(model.image_width, int)
assert isinstance(model.image_height, int)
+143
View File
@@ -0,0 +1,143 @@
from pathlib import Path
import pytest
from cbz.page import PageInfo
from cbz.constants import PageType
class TestPageInfo:
"""Test cases for PageInfo class."""
def test_load_from_file(self, sample_image_path: Path) -> None:
"""Test loading PageInfo from image file."""
page = PageInfo.load(path=sample_image_path)
assert page is not None
assert isinstance(page.content, bytes)
assert len(page.content) > 0
assert page.name == sample_image_path.name
assert page.image_width > 0
assert page.image_height > 0
assert page.image_size > 0
assert page.suffix is not None
def test_load_with_page_type(self, sample_image_path: Path) -> None:
"""Test loading PageInfo with specific page type."""
page = PageInfo.load(path=sample_image_path, type=PageType.FRONT_COVER)
assert page.type == PageType.FRONT_COVER
def test_load_with_custom_name(self, sample_image_path: Path) -> None:
"""Test loading PageInfo with custom name."""
custom_name = 'custom_page.jpg'
page = PageInfo.load(path=sample_image_path, name=custom_name)
assert page.name == custom_name
def test_page_content_property(self, sample_image_path: Path) -> None:
"""Test page content property getter and setter."""
page = PageInfo.load(path=sample_image_path)
original_content = page.content
# Test getter
assert page.content == original_content
assert isinstance(page.content, bytes)
# Test that content is properly set and metadata extracted
assert page.image_width > 0
assert page.image_height > 0
assert page.image_size > 0
def test_image_metadata_extraction(self, sample_image_path: Path) -> None:
"""Test that image metadata is correctly extracted."""
page = PageInfo.load(path=sample_image_path)
# Check that all image metadata properties are set
assert hasattr(page, 'image_width') and page.image_width > 0
assert hasattr(page, 'image_height') and page.image_height > 0
assert hasattr(page, 'image_size') and page.image_size > 0
assert hasattr(page, 'suffix') and page.suffix is not None
# Verify dimensions make sense for an image
assert isinstance(page.image_width, int)
assert isinstance(page.image_height, int)
assert isinstance(page.image_size, int)
def test_multiple_image_formats(self, images_dir: Path) -> None:
"""Test loading different image formats."""
image_files = list(images_dir.glob('*.jpg'))
if not image_files:
pytest.skip('No image files found in example directory')
for image_path in image_files[:3]: # Test first 3 images
page = PageInfo.load(path=image_path)
assert page is not None
assert isinstance(page.content, bytes)
assert len(page.content) > 0
assert page.image_width > 0
assert page.image_height > 0
assert page.image_size > 0
def test_page_type_assignment(self, sample_image_path: Path) -> None:
"""Test different page type assignments."""
page_types = [
PageType.FRONT_COVER,
PageType.INNER_COVER,
PageType.ROUNDUP,
PageType.STORY,
PageType.ADVERTISEMENT,
PageType.EDITORIAL,
PageType.LETTERS,
PageType.PREVIEW,
PageType.BACK_COVER,
PageType.OTHER,
PageType.DELETED
]
for page_type in page_types:
page = PageInfo.load(path=sample_image_path, type=page_type)
assert page.type == page_type
def test_page_creation_from_bytes(self, sample_image_path: Path) -> None:
"""Test creating PageInfo directly from bytes."""
# Read image file as bytes
with open(sample_image_path, 'rb') as f:
image_bytes = f.read()
# Create page from bytes
page = PageInfo(content=image_bytes, name='test_page.jpg')
assert page.content == image_bytes
assert page.name == 'test_page.jpg'
assert page.image_width > 0
assert page.image_height > 0
assert page.image_size > 0
def test_repr_string(self, sample_image_path: Path) -> None:
"""Test string representation of PageInfo."""
page = PageInfo.load(path=sample_image_path, type=PageType.STORY)
repr_str = repr(page)
assert 'PageInfo' in repr_str
assert isinstance(repr_str, str)
def test_page_bookmark_property(self, sample_image_path: Path) -> None:
"""Test page bookmark property."""
# Test with bookmark
page = PageInfo.load(path=sample_image_path, bookmark='Chapter 1')
assert page.bookmark == 'Chapter 1'
# Test without bookmark
page_no_bookmark = PageInfo.load(path=sample_image_path)
assert hasattr(page_no_bookmark, 'bookmark')
def test_page_double_page_property(self, sample_image_path: Path) -> None:
"""Test page double_page property."""
page = PageInfo.load(path=sample_image_path, double=True)
assert page.double
page_no_double = PageInfo.load(path=sample_image_path)
assert hasattr(page_no_double, 'double')