Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45da4c11f4 | ||
|
|
1e23fad6b5 | ||
|
|
4ebee2a6cf | ||
|
|
ef4eff4868 | ||
|
|
d37be87c2f | ||
|
|
0b011a7efc | ||
|
|
62b8cf5739 | ||
|
|
871716971a | ||
|
|
597cb41b00 | ||
|
|
a489ae17fb | ||
|
|
abc69d4673 | ||
|
|
6a1111a6bb | ||
|
|
220f19fe46 | ||
|
|
bbab9ba3b3 | ||
|
|
5824a8428a | ||
|
|
23f7e37561 | ||
|
|
5cc4732238 | ||
|
|
9d5d539311 | ||
|
|
f8274ae26b | ||
|
|
c045c548de | ||
|
|
7355e5edc0 | ||
|
|
5eee972d75 | ||
|
|
3604a4e98f | ||
|
|
19c1b01ea6 | ||
|
|
7e599a1adb | ||
|
|
312eb2da13 | ||
|
|
b7d1b631b9 | ||
|
|
fe43024656 | ||
|
|
58a5256dc1 | ||
|
|
7d0f004345 | ||
|
|
6aeabc2047 | ||
|
|
46b508adf8 | ||
|
|
6d771e3bf8 | ||
|
|
0c4aa34f8f | ||
|
|
48c99ba44f | ||
|
|
8af1c47e15 | ||
|
|
6aa6b0da21 | ||
|
|
526fee3617 | ||
|
|
6354c1d3bb | ||
|
|
3a80dfe47b | ||
|
|
58ef27f227 | ||
|
|
36b76f6d9e | ||
|
|
3c53f16d07 | ||
|
|
8161551ab6 | ||
|
|
8ab1b37332 | ||
|
|
784f698044 | ||
|
|
fc1e693e67 | ||
|
|
4d831f28a9 | ||
|
|
195fca4370 | ||
|
|
758e54fa4d | ||
|
|
7f1a0724e1 | ||
|
|
f015d998e1 | ||
|
|
7d82752546 | ||
|
|
e8e6eaf3ae | ||
|
|
4f777bc9bf | ||
|
|
c48d64cbe0 | ||
|
|
d195e25043 | ||
|
|
5fd7c019d1 | ||
|
|
c4169ebb84 | ||
|
|
a46bae2e2d | ||
|
|
616b1eff8e | ||
|
|
27a313fe4c | ||
|
|
72c14449fc | ||
|
|
04feaa7027 | ||
|
|
8b8069e38d | ||
|
|
d357ad3e1d |
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -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@v6
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v6
|
||||
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
|
||||
@@ -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.
|
||||
@@ -174,3 +174,4 @@ pyrightconfig.json
|
||||
*.cbz
|
||||
*.pdf
|
||||
*.zip
|
||||
*cbr
|
||||
@@ -4,6 +4,83 @@ 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.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
|
||||
|
||||
### 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
|
||||
|
||||
- Fix `Path` import using `pathlib`.
|
||||
|
||||
## [3.3.4] - 2024-07-18
|
||||
|
||||
### Fixed
|
||||
@@ -155,6 +232,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
- 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.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
|
||||
[3.3.2]: https://github.com/hyugogirubato/cbz/releases/tag/v3.3.2
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,59 @@ 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).
|
||||
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
|
||||
- **[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
|
||||
- **[rarfile](https://pypi.org/project/rarfile/)** - ISC License
|
||||
- **[xmltodict](https://pypi.org/project/xmltodict/)** - MIT License
|
||||
|
||||
---
|
||||
|
||||
© hyugogirubato 2025
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
from .comic import ComicInfo
|
||||
from .page import PageInfo
|
||||
|
||||
# Register optional Pillow plugins
|
||||
for _plugin in ('pillow_avif', 'pillow_jxl'):
|
||||
try:
|
||||
__import__(_plugin)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
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 +25,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 +69,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 +105,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 +118,94 @@ 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 = sorted(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
|
||||
|
||||
# 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 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:
|
||||
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 +217,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 +259,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 +271,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 +289,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()
|
||||
|
||||
@@ -3,189 +3,195 @@ from typing import Union
|
||||
|
||||
from langcodes import Language
|
||||
|
||||
XML_NAME = 'ComicInfo.xml'
|
||||
XML_NAME = "ComicInfo.xml"
|
||||
IMAGE_FORMAT = {
|
||||
'.jpeg', '.jpg', # Joint Photographic Experts Group
|
||||
'.png', # Portable Network Graphics
|
||||
'.gif', # Graphics Interchange Format
|
||||
'.bmp', # Bitmap Image File
|
||||
'.tiff', '.tif', # Tagged Image File Format
|
||||
'.webp' # Web Picture Format
|
||||
".jpeg",
|
||||
".jpg", # JPEG (Joint Photographic Experts Group)
|
||||
".png", # PNG (Portable Network Graphics)
|
||||
".gif", # GIF (Graphics Interchange Format)
|
||||
".bmp", # BMP (Bitmap Image File)
|
||||
".tiff",
|
||||
".tif", # TIFF (Tagged Image File 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)
|
||||
}
|
||||
|
||||
|
||||
class YesNo(Enum):
|
||||
UNKNOWN = 'Unknown'
|
||||
NO = 'No'
|
||||
YES = 'Yes'
|
||||
UNKNOWN = "Unknown"
|
||||
NO = "No"
|
||||
YES = "Yes"
|
||||
|
||||
|
||||
class Manga(Enum):
|
||||
UNKNOWN = 'Unknown'
|
||||
NO = 'No'
|
||||
YES = 'Yes'
|
||||
RIGHT_LEFT = 'YesAndRightToLeft'
|
||||
UNKNOWN = "Unknown"
|
||||
NO = "No"
|
||||
YES = "Yes"
|
||||
RIGHT_LEFT = "YesAndRightToLeft"
|
||||
|
||||
|
||||
class PageType(Enum):
|
||||
FRONT_COVER = 'FrontCover'
|
||||
INNER_COVER = 'InnerCover'
|
||||
ROUNDUP = 'Roundup'
|
||||
STORY = 'Story'
|
||||
ADVERTISEMENT = 'Advertisement'
|
||||
EDITORIAL = 'Editorial'
|
||||
LETTERS = 'Letters'
|
||||
PREVIEW = 'Preview'
|
||||
BACK_COVER = 'BackCover'
|
||||
OTHER = 'Other'
|
||||
DELETED = 'Deleted'
|
||||
FRONT_COVER = "FrontCover"
|
||||
INNER_COVER = "InnerCover"
|
||||
ROUNDUP = "Roundup"
|
||||
STORY = "Story"
|
||||
ADVERTISEMENT = "Advertisement"
|
||||
EDITORIAL = "Editorial"
|
||||
LETTERS = "Letters"
|
||||
PREVIEW = "Preview"
|
||||
BACK_COVER = "BackCover"
|
||||
OTHER = "Other"
|
||||
DELETED = "Deleted"
|
||||
|
||||
|
||||
class AgeRating(Enum):
|
||||
UNKNOWN = 'Unknown'
|
||||
ADULTS18 = 'Adults Only 18+'
|
||||
CHILDHOOD = 'Early Childhood'
|
||||
EVERYONE = 'Everyone'
|
||||
EVERYONE10 = 'Everyone 10+'
|
||||
G = 'G'
|
||||
KIDS = 'Kids to Adults'
|
||||
M = 'M'
|
||||
MA15 = 'MA15+'
|
||||
MATURE17 = 'Mature 17+'
|
||||
PG = 'PG'
|
||||
R18 = 'R18+'
|
||||
PENDING = 'Rating Pending'
|
||||
TEEN = 'Teen'
|
||||
X18 = 'X18+'
|
||||
UNKNOWN = "Unknown"
|
||||
ADULTS18 = "Adults Only 18+"
|
||||
CHILDHOOD = "Early Childhood"
|
||||
EVERYONE = "Everyone"
|
||||
EVERYONE10 = "Everyone 10+"
|
||||
G = "G"
|
||||
KIDS = "Kids to Adults"
|
||||
M = "M"
|
||||
MA15 = "MA15+"
|
||||
MATURE17 = "Mature 17+"
|
||||
PG = "PG"
|
||||
R18 = "R18+"
|
||||
PENDING = "Rating Pending"
|
||||
TEEN = "Teen"
|
||||
X18 = "X18+"
|
||||
|
||||
|
||||
class Format(Enum):
|
||||
UNKNOWN = 'Unknown'
|
||||
UNKNOWN = "Unknown"
|
||||
# ONE_SHOT = '1 Shot'
|
||||
# ONE_SHOT = '1/2',
|
||||
# ONE_SHOT = '1-Shot'
|
||||
ANNOTATION = 'Annotation'
|
||||
ANNOTATION = "Annotation"
|
||||
# ANNOTATIONS = 'Annotations'
|
||||
ANNUAL = 'Annual'
|
||||
ANTHOLOGY = 'Anthology'
|
||||
ANNUAL = "Annual"
|
||||
ANTHOLOGY = "Anthology"
|
||||
# BLACK_WHITE = 'B&W'
|
||||
# BLACK_WHITE = 'B/W'
|
||||
# BLACK_WHITE = 'B&&W'
|
||||
BLACK_WHITE = 'Black & White'
|
||||
BLACK_WHITE = "Black & White"
|
||||
# BOX_SET = 'Box Set'
|
||||
BOX_SET = 'Box-Set'
|
||||
CROSSOVER = 'Crossover'
|
||||
DIRECTOR_CUT = 'Director\'s Cut'
|
||||
EPILOGUE = 'Epilogue'
|
||||
EVENT = 'Event'
|
||||
FCBD = 'FCBD'
|
||||
FLYER = 'Flyer'
|
||||
BOX_SET = "Box-Set"
|
||||
CROSSOVER = "Crossover"
|
||||
DIRECTOR_CUT = "Director's Cut"
|
||||
EPILOGUE = "Epilogue"
|
||||
EVENT = "Event"
|
||||
FCBD = "FCBD"
|
||||
FLYER = "Flyer"
|
||||
# GIANT_SIZE = 'Giant'
|
||||
# GIANT_SIZE = 'Giant Size'
|
||||
GIANT_SIZE = 'Giant-Size'
|
||||
GRAPHIC_NOVEL = 'Graphic Novel'
|
||||
GIANT_SIZE = "Giant-Size"
|
||||
GRAPHIC_NOVEL = "Graphic Novel"
|
||||
# HARDCOVER = 'Hardcover'
|
||||
HARDCOVER = 'Hard-Cover'
|
||||
HARDCOVER = "Hard-Cover"
|
||||
# KING_SIZE = 'King'
|
||||
# KING_SIZE = 'King Size'
|
||||
KING_SIZE = 'King-Size'
|
||||
LIMITED_SERIES = 'Limited Series'
|
||||
MAGAZINE = 'Magazine'
|
||||
NSFW = 'NSFW'
|
||||
KING_SIZE = "King-Size"
|
||||
LIMITED_SERIES = "Limited Series"
|
||||
MAGAZINE = "Magazine"
|
||||
NSFW = "NSFW"
|
||||
# ONE_SHOT = 'One Shot'
|
||||
ONE_SHOT = 'One-Shot'
|
||||
POINT1 = 'Point 1'
|
||||
PREVIEW = 'Preview'
|
||||
PROLOGUE = 'Prologue'
|
||||
REFERENCE = 'Reference'
|
||||
REVIEW = 'Review'
|
||||
REVIEWED = 'Reviewed'
|
||||
SCANLATION = 'Scanlation'
|
||||
SCRIPT = 'Script'
|
||||
SERIES = 'Series'
|
||||
SKETCH = 'Sketch'
|
||||
SPECIAL = 'Special'
|
||||
ONE_SHOT = "One-Shot"
|
||||
POINT1 = "Point 1"
|
||||
PREVIEW = "Preview"
|
||||
PROLOGUE = "Prologue"
|
||||
REFERENCE = "Reference"
|
||||
REVIEW = "Review"
|
||||
REVIEWED = "Reviewed"
|
||||
SCANLATION = "Scanlation"
|
||||
SCRIPT = "Script"
|
||||
SERIES = "Series"
|
||||
SKETCH = "Sketch"
|
||||
SPECIAL = "Special"
|
||||
# TRADE_PAPER_BACK = 'TPB'
|
||||
TRADE_PAPER_BACK = 'Trade Paper Back'
|
||||
TRADE_PAPER_BACK = "Trade Paper Back"
|
||||
# WEB_COMIC = 'WebComic'
|
||||
WEB_COMIC = 'Web Comic'
|
||||
WEB_COMIC = "Web Comic"
|
||||
# YEAR_ONE = 'Year 1'
|
||||
YEAR_ONE = 'Year One'
|
||||
YEAR_ONE = "Year One"
|
||||
|
||||
|
||||
class Rating(float):
|
||||
|
||||
def __new__(cls, value: Union[int, float] = -1) -> float:
|
||||
assert -1 <= float(value) <= 5, f'Rating must be between 0 and 5, input {value}'
|
||||
assert -1 <= float(value) <= 5, f"Rating must be between 0 and 5, input {value}"
|
||||
return super().__new__(cls, value)
|
||||
|
||||
|
||||
class LanguageISO(str):
|
||||
|
||||
def __new__(cls, value: str = '') -> str:
|
||||
assert not value or Language.get(str(value)).is_valid(), f'Invalid {value} language'
|
||||
def __new__(cls, value: str = "") -> str:
|
||||
assert (
|
||||
not value or Language.get(str(value)).is_valid()
|
||||
), f"Invalid {value} language"
|
||||
return super().__new__(cls, value)
|
||||
|
||||
|
||||
COMIC_FIELDS = {
|
||||
'title': ('Title', str),
|
||||
'series': ('Series', str),
|
||||
'number': ('Number', int),
|
||||
'count': ('Count', int),
|
||||
'volume': ('Volume', int),
|
||||
'alternate_series': ('AlternateSeries', str),
|
||||
'alternate_number': ('AlternateNumber', int),
|
||||
'alternate_count': ('AlternateCount', int),
|
||||
'summary': ('Summary', str),
|
||||
'notes': ('Notes', str),
|
||||
'year': ('Year', int),
|
||||
'month': ('Month', int),
|
||||
'day': ('Day', int),
|
||||
'writer': ('Writer', str),
|
||||
'penciller': ('Penciller', str),
|
||||
'inker': ('Inker', str),
|
||||
'colorist': ('Colorist', str),
|
||||
'letterer': ('Letterer', str),
|
||||
'cover_artist': ('CoverArtist', str),
|
||||
'editor': ('Editor', str),
|
||||
'translator': ('Translator', str),
|
||||
'publisher': ('Publisher', str),
|
||||
'imprint': ('Imprint', str),
|
||||
'genre': ('Genre', str),
|
||||
'tags': ('Tags', str),
|
||||
'web': ('Web', str),
|
||||
'format': ('Format', Format),
|
||||
'ean': ('EAN', str),
|
||||
'black_white': ('BlackAndWhite', YesNo),
|
||||
'manga': ('Manga', Manga),
|
||||
'characters': ('Characters', str),
|
||||
'teams': ('Teams', str),
|
||||
'locations': ('Locations', str),
|
||||
'scan_information': ('ScanInformation', str),
|
||||
'story_arc': ('StoryArc', str),
|
||||
'story_arc_number': ('StoryArcNumber', int),
|
||||
'series_group': ('SeriesGroup', str),
|
||||
'age_rating': ('AgeRating', AgeRating),
|
||||
'main_character_or_team': ('MainCharacterOrTeam', str),
|
||||
'review': ('Review', str),
|
||||
'language_iso': ('LanguageISO', LanguageISO),
|
||||
'community_rating': ('CommunityRating', Rating),
|
||||
'added': ('Added', str),
|
||||
'released': ('Released', str),
|
||||
'file_size': ('FileSize', int),
|
||||
'file_modified_time': ('FileModifiedTime', str),
|
||||
'file_creation_time': ('FileCreationTime', str),
|
||||
'book_price': ('BookPrice', str),
|
||||
'custom_values_store': ('CustomValuesStore', str)
|
||||
"title": ("Title", str),
|
||||
"series": ("Series", str),
|
||||
"number": ("Number", float),
|
||||
"count": ("Count", int),
|
||||
"volume": ("Volume", int),
|
||||
"alternate_series": ("AlternateSeries", str),
|
||||
"alternate_number": ("AlternateNumber", int),
|
||||
"alternate_count": ("AlternateCount", int),
|
||||
"summary": ("Summary", str),
|
||||
"notes": ("Notes", str),
|
||||
"year": ("Year", int),
|
||||
"month": ("Month", int),
|
||||
"day": ("Day", int),
|
||||
"writer": ("Writer", str),
|
||||
"penciller": ("Penciller", str),
|
||||
"inker": ("Inker", str),
|
||||
"colorist": ("Colorist", str),
|
||||
"letterer": ("Letterer", str),
|
||||
"cover_artist": ("CoverArtist", str),
|
||||
"editor": ("Editor", str),
|
||||
"translator": ("Translator", str),
|
||||
"publisher": ("Publisher", str),
|
||||
"imprint": ("Imprint", str),
|
||||
"genre": ("Genre", str),
|
||||
"tags": ("Tags", str),
|
||||
"web": ("Web", str),
|
||||
"format": ("Format", Format),
|
||||
"ean": ("EAN", str),
|
||||
"black_white": ("BlackAndWhite", YesNo),
|
||||
"manga": ("Manga", Manga),
|
||||
"characters": ("Characters", str),
|
||||
"teams": ("Teams", str),
|
||||
"locations": ("Locations", str),
|
||||
"scan_information": ("ScanInformation", str),
|
||||
"story_arc": ("StoryArc", str),
|
||||
"story_arc_number": ("StoryArcNumber", int),
|
||||
"series_group": ("SeriesGroup", str),
|
||||
"age_rating": ("AgeRating", AgeRating),
|
||||
"main_character_or_team": ("MainCharacterOrTeam", str),
|
||||
"review": ("Review", str),
|
||||
"language_iso": ("LanguageISO", LanguageISO),
|
||||
"community_rating": ("CommunityRating", Rating),
|
||||
"added": ("Added", str),
|
||||
"released": ("Released", str),
|
||||
"file_size": ("FileSize", int),
|
||||
"file_modified_time": ("FileModifiedTime", str),
|
||||
"file_creation_time": ("FileCreationTime", str),
|
||||
"book_price": ("BookPrice", str),
|
||||
"custom_values_store": ("CustomValuesStore", str),
|
||||
}
|
||||
|
||||
PAGE_FIELDS = {
|
||||
'image': ('@Image', int),
|
||||
'type': ('@Type', PageType),
|
||||
'double': ('@DoublePage', bool),
|
||||
'key': ('@Key', str),
|
||||
'bookmark': ('@Bookmark', str),
|
||||
'image_size': ('@ImageSize', int),
|
||||
'image_width': ('@ImageWidth', int),
|
||||
'image_height': ('@ImageHeight', int)
|
||||
"image": ("@Image", int),
|
||||
"type": ("@Type", PageType),
|
||||
"double": ("@DoublePage", bool),
|
||||
"key": ("@Key", str),
|
||||
"bookmark": ("@Bookmark", str),
|
||||
"image_size": ("@ImageSize", int),
|
||||
"image_width": ("@ImageWidth", int),
|
||||
"image_height": ("@ImageHeight", int),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
from cbz.constants import COMIC_FIELDS, PAGE_FIELDS, Format, YesNo, Manga, AgeRating, LanguageISO, Rating, PageType
|
||||
from cbz.constants import (
|
||||
COMIC_FIELDS,
|
||||
PAGE_FIELDS,
|
||||
Format,
|
||||
YesNo,
|
||||
Manga,
|
||||
AgeRating,
|
||||
LanguageISO,
|
||||
Rating,
|
||||
PageType,
|
||||
)
|
||||
from cbz.utils import verify_attr, default_attr
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseModel:
|
||||
@@ -21,7 +35,7 @@ class BaseModel:
|
||||
# Set default values for each attribute based on its type
|
||||
setattr(self, key, kwargs.get(key, default_attr(field_type)))
|
||||
|
||||
def __setattr__(self, key: str, value: any) -> None:
|
||||
def __setattr__(self, key: str, value: "Any") -> None:
|
||||
"""
|
||||
Sets the value of an attribute and verifies its type.
|
||||
|
||||
@@ -39,6 +53,9 @@ class BaseModel:
|
||||
value = field_type(value)
|
||||
# Verify that the assigned value matches the expected type
|
||||
verify_attr(field_type, key, value)
|
||||
if isinstance(value, float):
|
||||
if value.is_integer():
|
||||
value = int(value)
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
super().__setattr__(key, value)
|
||||
@@ -50,9 +67,15 @@ class BaseModel:
|
||||
Returns:
|
||||
str: A string representation of the object, displaying its class name and attribute key-value pairs.
|
||||
"""
|
||||
return '{name}({items})'.format(
|
||||
return "{name}({items})".format(
|
||||
name=self.__class__.__name__,
|
||||
items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items() if not k.startswith('_')])
|
||||
items=", ".join(
|
||||
[
|
||||
f"{k}={repr(v)}"
|
||||
for k, v in self.__dict__.items()
|
||||
if not k.startswith("_")
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -60,9 +83,10 @@ class ComicModel(BaseModel):
|
||||
"""
|
||||
Model for representing comic book metadata.
|
||||
"""
|
||||
|
||||
title: str
|
||||
series: str
|
||||
number: int
|
||||
number: float
|
||||
count: int
|
||||
volume: int
|
||||
alternate_series: str
|
||||
@@ -124,6 +148,7 @@ class PageModel(BaseModel):
|
||||
"""
|
||||
Model for representing comic book pages.
|
||||
"""
|
||||
|
||||
type: PageType
|
||||
double: bool
|
||||
image_size: int
|
||||
@@ -132,6 +157,7 @@ class PageModel(BaseModel):
|
||||
image_width: int
|
||||
image_height: int
|
||||
suffix: str
|
||||
name: str
|
||||
__content: bytes
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
||||
@@ -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
|
||||
@@ -73,6 +75,8 @@ class PageInfo(PageModel):
|
||||
data = base64.b64decode(data)
|
||||
if not isinstance(data, bytes):
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
@@ -93,8 +97,10 @@ 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:
|
||||
return cls(f.read(), **kwargs)
|
||||
path = Path(path)
|
||||
with path.open(mode='rb') as f:
|
||||
kwargs.setdefault('name', path.name)
|
||||
return cls.loads(f.read(), **kwargs)
|
||||
|
||||
def show(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 path 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.
|
||||
|
||||
@@ -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.0.3"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-71.0.3-py3-none-any.whl", hash = "sha256:f501b6e6db709818dc76882582d9c516bf3b67b948864c5fa1d1624c09a49207"},
|
||||
{file = "setuptools-71.0.3.tar.gz", hash = "sha256:3d8531791a27056f4a38cd3e54084d8b1c4228ff9cf3f2d7dd075ec99f9fd70d"},
|
||||
]
|
||||
|
||||
[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 (<7.4)", "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.10.0)", "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"
|
||||
@@ -1,15 +1,20 @@
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "cbz"
|
||||
version = "3.3.4"
|
||||
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.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."
|
||||
license = "MIT"
|
||||
authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"]
|
||||
authors = [{name="hyugogirubato", email="65763543+hyugogirubato@users.noreply.github.com"}]
|
||||
readme = "README.md"
|
||||
keywords = ["python", "cbz", "ebooks", "manga", "comics", "webtoons"]
|
||||
keywords = [
|
||||
"python",
|
||||
"cbz",
|
||||
"cbr",
|
||||
"pdf",
|
||||
"ebooks",
|
||||
"manga",
|
||||
"comics",
|
||||
"webtoons",
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
@@ -17,32 +22,37 @@ 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" }
|
||||
requires-python = ">=3.8,<4.0"
|
||||
|
||||
dependencies= [
|
||||
"langcodes>=3.4.0",
|
||||
"Pillow>=10.4.0",
|
||||
"pypdf >=5.7.0",
|
||||
"rarfile>=4.2",
|
||||
"xmltodict>=0.14.2"
|
||||
]
|
||||
|
||||
|
||||
[tool.poetry.urls]
|
||||
"Issues" = "https://github.com/hyugogirubato/cbz/issues"
|
||||
"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"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
cbzplayer = "cbz.__main__:main"
|
||||
[project.optional-dependencies]
|
||||
pillow = ["pillow-avif-plugin>=1.5.2", "pillow-jxl-plugin>=1.3.4"]
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "localpypi"
|
||||
url = "https://pypi.org/simple/"
|
||||
priority = "primary"
|
||||
[dependency-groups]
|
||||
dev = ["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 }
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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
|
||||
@@ -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)
|
||||
@@ -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')
|
||||