Merge pull request #7 from piskunqa/main

Implementation of preview book, by analogy of pillow.Image.show
This commit is contained in:
hyugogirubato
2024-07-03 07:51:58 +02:00
committed by GitHub
6 changed files with 285 additions and 50 deletions
+75 -1
View File
@@ -4,6 +4,7 @@ import json
import zipfile
from io import BytesIO
from pathlib import Path
from typing import Union
import xmltodict
@@ -12,6 +13,7 @@ from cbz import utils
from cbz.constants import xml_name
from cbz.models import ComicModel
from cbz.page import PageInfo
from cbz.ui import show_in_tk
class ComicInfo(ComicModel):
@@ -32,6 +34,13 @@ class ComicInfo(ComicModel):
def from_pages(cls, pages: [PageInfo], **kwargs) -> ComicInfo:
return cls(pages, **kwargs)
def show(self):
"""
display cbz for preview, after call open ui with info and pages and wait for close preview windows
:return:
"""
show_in_tk(self.title, self.__pages, utils.dumps(self._get()))
@classmethod
def from_cbz(cls, path: Union[Path, str]) -> ComicInfo:
"""
@@ -104,9 +113,74 @@ class ComicInfo(ComicModel):
xml_name,
xmltodict.unparse({'ComicInfo': self.dumps()}, pretty=True).encode('utf-8')
)
for i, page in enumerate(self.__pages):
zip_file.writestr(f'page-{i + 1:03d}{page.suffix}', page.content)
result = zip_buffer.getvalue()
zip_buffer.close()
return result
def get_page(self, index: int) -> PageInfo:
"""
Get page by index
:param index:
:return:
"""
return self.__pages[index]
def show_page(self, index: int) -> None:
"""
Display page by index
:param index:
:return:
"""
self.__pages[index].show()
def delete_page(self, index: int) -> PageInfo:
"""
Delete page by index
:param index:
:return: deleted page
"""
return self.__pages.pop(index)
def add_page(self, page: PageInfo) -> list[PageInfo]:
"""
Add new page to the book
:param page:
:return: new list of pages
"""
self.__pages.append(page)
return self.__pages
def insert_page(self, index: int, page: PageInfo) -> list[PageInfo]:
"""
Add new page to position
:param page:
:param index:
:return:
"""
self.__pages.insert(index, page)
return self.__pages
def get_pages_count(self) -> int:
"""
Get count of pages
:return:
"""
return len(self.__pages)
def get_all_pages(self) -> list[PageInfo]:
"""
Get all pages of book
:return:
"""
return self.__pages
def save_page(self, index: int, path: Union[Path, str]) -> None:
"""
Save page to the file
:param index: page index
:param path: path to new file, str or Path
:return:
"""
self.__pages[index].save(path)
+12
View File
@@ -162,3 +162,15 @@ FIELDS = (
(("language_iso", "LanguageISO"), ("language_iso", (ValidLanguage, str))),
(("community_rating", "CommunityRating"), ("community_rating", (ValidRating, float, int, str))),
)
PAGE_FIELDS = (
# model: (key, xml key): (variable name, (expected type, second expected type,...))
# in case of multiple expected formats, value cast to first type in tuple
(("type", "Type"), ("type", (PageType, str))),
(("double", "DoublePage"), ("double", (bool,))),
(("key", "Key"), ("key", (str,))),
(("bookmark", "Bookmark"), ("bookmark", (str,))),
(("image_size", "ImageSize"), ("_image_size", (int,))),
(("image_width", "ImageWidth"), ("_image_width", (int,))),
(("image_height", "ImageHeight"), ("_image_height", (int,))),
)
+22 -32
View File
@@ -1,5 +1,5 @@
from cbz.constants import YesNo, Manga, AgeRating, Format, FIELDS
from typing import Any
from cbz.constants import YesNo, Manga, AgeRating, Format, PageType, FIELDS, PAGE_FIELDS
from cbz.utils import _get, _set
class ComicModel:
@@ -48,35 +48,25 @@ class ComicModel:
_filepath: str = ""
def __init__(self, kwargs: dict):
"""
Set class variables from input dictionary, check types of input and cast values to correct type
:param kwargs: dictionary of input data
"""
for kwarg_key, kwarg_value in kwargs.items():
for keys, values in FIELDS:
if kwarg_key in keys:
variable, types = values
if hasattr(self, variable):
setattr(self, variable, self._check_type(kwarg_key, kwarg_value, types))
break
@staticmethod
def _check_type(key: str, value: Any, types: tuple) -> Any:
"""
Check type of value, and cast this value to first type of the types
:param key: name of key of variable (using only for best error information)
:param value: value for a check
:param types: list of the allowed types
:return: value with new type
"""
if not isinstance(value, types):
raise ValueError(
f"Unexpected type of {key}, got: {type(value).__name__}, expected: {[i.__name__ for i in types]}")
return types[0](value)
_set(self, FIELDS, kwargs)
def _get(self) -> dict:
"""
Create dictionary from variables by FIELDS
:return: dictionary with variables value
"""
return {key[1]: self._check_type(value[0], getattr(self, value[0]), value[1]) for key, value in FIELDS}
return _get(self, FIELDS)
class PageModel:
suffix: str
_content: bytes
type: PageType = PageType.STORY
double: bool = False
_image_size: int = 0
key: str = ""
bookmark: str = ""
_image_width: int = 0
_image_height: int = 0
def __init__(self, kwargs: dict):
_set(self, PAGE_FIELDS, kwargs)
def _get(self) -> dict:
return _get(self, PAGE_FIELDS)
+44 -17
View File
@@ -3,35 +3,45 @@ from __future__ import annotations
import base64
import json
from io import BytesIO
from typing import Union
from pathlib import Path
from typing import Union
from PIL import Image
from cbz import utils
from cbz.constants import PageType
from cbz.models import PageModel
class PageInfo:
class PageInfo(PageModel):
def __init__(self, content: bytes, **kwargs):
with Image.open(BytesIO(content)) as image:
self.suffix = f'.{image.format.lower()}'
super(PageInfo, self).__init__(kwargs)
self.content = content
self.__info = {
# 'Image': 0,
'Type': PageType(kwargs.get('type', PageType.STORY)),
'DoublePage': bool(kwargs.get('double', False)),
'ImageSize': len(content),
'Key': str(kwargs.get('key', '')),
'Bookmark': str(kwargs.get('bookmark', '')),
'ImageWidth': int(image.width),
'ImageHeight': int(image.height)
}
@property
def content(self) -> bytes:
"""
content getter
:return: image content
"""
return self._content
@content.setter
def content(self, value) -> None:
"""
Set image info by current content
:param value:
:return:
"""
with Image.open(BytesIO(value)) as image:
self.suffix = f'.{image.format.lower()}'
self._image_width = int(image.width)
self._image_height = int(image.height)
self._image_size = len(value)
self._content = value
def dumps(self) -> dict:
return utils.dumps(self.__info)
return utils.dumps(self._get())
def __repr__(self) -> str:
return json.dumps(self.dumps(), indent=2)
@@ -50,3 +60,20 @@ class PageInfo:
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)
def show(self) -> None:
"""
display this page
:return:
"""
with Image.open(BytesIO(self.content)) as image:
image.show()
def save(self, path: Union[Path, str]):
"""
Save page to file
:param path:
:return:
"""
with Path(path).open(mode='wb') as f:
f.write(self.content)
+95
View File
@@ -0,0 +1,95 @@
from io import BytesIO
from tkinter import (Tk, Button, BOTTOM, TOP, PhotoImage, Scrollbar, X, Y, Canvas, HORIZONTAL, VERTICAL, YES, BOTH,
SUNKEN, RIGHT, LEFT, DISABLED, NORMAL, Label, Frame, ALL)
from PIL import ImageTk, Image
class ScrolledCanvas(Frame):
image_cursor: int = -2
info_text: str = ""
pages = list()
next_btn = None
prev_btn = None
counter = None
image: PhotoImage
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.master.title("Spectrogram Viewer")
self.pack(expand=YES, fill=BOTH)
self.canvas = Canvas(self, relief=SUNKEN)
self.canvas.config(width=400, height=200)
self.canvas.config(highlightthickness=0)
s_bar_v = Scrollbar(self, orient=VERTICAL)
s_bar_h = Scrollbar(self, orient=HORIZONTAL)
s_bar_v.config(command=self.canvas.yview)
s_bar_h.config(command=self.canvas.xview)
self.canvas.config(yscrollcommand=s_bar_v.set)
self.canvas.config(xscrollcommand=s_bar_h.set)
s_bar_v.pack(side=RIGHT, fill=Y)
s_bar_h.pack(side=BOTTOM, fill=X)
self.canvas.pack(side=LEFT, expand=YES, fill=BOTH)
def set_text(self) -> None:
self.canvas.delete('image')
self.canvas.create_text(0, 0, anchor="nw", text=self.info_text, tags="image")
self.canvas.config(scrollregion=self.canvas.bbox(ALL))
def set_image(self, content: bytes) -> None:
self.canvas.delete('image')
with BytesIO(content) as f:
with Image.open(f) as image:
self.image = ImageTk.PhotoImage(image)
self.canvas.create_image(0, 0, anchor="nw", image=self.image, tags='image')
self.canvas.config(scrollregion=self.canvas.bbox(ALL))
def show_image(self, n: int) -> None:
self.image_cursor += n
if self.image_cursor == -1:
self.set_text()
self.prev_btn.config(state=DISABLED)
self.next_btn.config(state=NORMAL if self.pages else DISABLED)
elif self.image_cursor >= 0:
self.set_image(self.pages[self.image_cursor].content)
if self.image_cursor == len(self.pages) - 1:
self.next_btn.config(state=DISABLED)
self.prev_btn.config(state=NORMAL)
else:
self.prev_btn.config(state=NORMAL)
self.next_btn.config(state=NORMAL)
self.counter.config(text=f"{self.image_cursor + 1}/{len(self.pages)}")
self.canvas.yview_moveto(0)
self.canvas.xview_moveto(0)
def show_in_tk(title, pages, info):
root = Tk()
root.geometry("700x700")
frame = ScrolledCanvas(root)
frame.info_text = "\n".join([f"{key}: {value}" for key, value in info.items()])
frame.pages = pages
frame.pack(side=TOP)
button_frame = Frame(root)
button_next = Button(button_frame, text="Next", command=lambda: frame.show_image(1))
button_exit = Button(button_frame, text="Exit", command=root.quit)
counter = Label(button_frame)
button_prev = Button(button_frame, text="Previous", command=lambda: frame.show_image(-1))
counter.grid(row=0, column=1)
button_exit.grid(row=0, column=2)
button_next.grid(row=0, column=3)
button_prev.grid(row=0, column=0)
frame.next_btn = button_next
frame.prev_btn = button_prev
frame.counter = counter
button_frame.pack(side=BOTTOM)
root.bind('<Left>', lambda x: frame.show_image(-1) if button_prev["state"] == NORMAL else True)
root.bind('<Right>', lambda x: frame.show_image(1) if button_next["state"] == NORMAL else True)
root.bind('<Escape>', lambda x: root.quit())
frame.show_image(1)
root.title(title)
root.mainloop()
+37
View File
@@ -1,5 +1,42 @@
from enum import Enum
from typing import Any
def dumps(data: dict) -> dict:
return {k: v.value if isinstance(v, Enum) else v for k, v in data.items() if v and v != -1 and v != 'Unknown'}
def _check_type(key: str, value: Any, types: tuple) -> Any:
"""
Check type of value, and cast this value to first type of the types
:param key: name of key of variable (using only for best error information)
:param value: value for a check
:param types: list of the allowed types
:return: value with new type
"""
if not isinstance(value, types):
raise ValueError(
f"Unexpected type of {key}, got: {type(value).__name__}, expected: {[i.__name__ for i in types]}")
return types[0](value)
def _get(obj: Any, fields: tuple) -> dict:
"""
Create dictionary from variables by PAGE_FIELDS
:return: dictionary with variables value
"""
return {key[1]: _check_type(value[0], getattr(obj, value[0]), value[1]) for key, value in fields}
def _set(obj: Any, fields: tuple, data_dict: dict) -> None:
"""
Set class variables from input dictionary, check types of input and cast values to correct type
:param kwargs: dictionary of input data
"""
for kwarg_key, kwarg_value in data_dict.items():
for keys, values in fields:
if kwarg_key in keys:
variable, types = values
if hasattr(obj, variable):
setattr(obj, variable, _check_type(kwarg_key, kwarg_value, types))
break