From 5515fd3660d4157f9791ed2f45e4fa844b34f048 Mon Sep 17 00:00:00 2001 From: Oleskii Pyskun Date: Tue, 2 Jul 2024 07:58:08 +0300 Subject: [PATCH 1/5] Implementation of preview book, by analogy of pillow.Image.show --- cbz/comic.py | 10 +++++- cbz/ui.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 cbz/ui.py diff --git a/cbz/comic.py b/cbz/comic.py index d81a95c..f0010ee 100644 --- a/cbz/comic.py +++ b/cbz/comic.py @@ -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,7 +113,6 @@ 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() diff --git a/cbz/ui.py b/cbz/ui.py new file mode 100644 index 0000000..1a48463 --- /dev/null +++ b/cbz/ui.py @@ -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('', lambda x: frame.show_image(-1) if button_prev["state"] == NORMAL else True) + root.bind('', lambda x: frame.show_image(1) if button_next["state"] == NORMAL else True) + root.bind('', lambda x: root.quit()) + frame.show_image(1) + root.title(title) + root.mainloop() From 2a508f0265a971e6c88957c63a6b626b6172e3eb Mon Sep 17 00:00:00 2001 From: Oleskii Pyskun Date: Tue, 2 Jul 2024 10:08:55 +0300 Subject: [PATCH 2/5] Implementation of page controller --- cbz/comic.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ cbz/constants.py | 12 ++++++++++ cbz/models.py | 54 +++++++++++++++++++-------------------------- cbz/page.py | 52 ++++++++++++++++++++++++++++--------------- cbz/utils.py | 37 +++++++++++++++++++++++++++++++ 5 files changed, 163 insertions(+), 49 deletions(-) diff --git a/cbz/comic.py b/cbz/comic.py index f0010ee..e840c36 100644 --- a/cbz/comic.py +++ b/cbz/comic.py @@ -118,3 +118,60 @@ class ComicInfo(ComicModel): 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 diff --git a/cbz/constants.py b/cbz/constants.py index 622a170..952f008 100644 --- a/cbz/constants.py +++ b/cbz/constants.py @@ -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,))), +) \ No newline at end of file diff --git a/cbz/models.py b/cbz/models.py index e602d12..2430a0c 100644 --- a/cbz/models.py +++ b/cbz/models.py @@ -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) diff --git a/cbz/page.py b/cbz/page.py index 320d687..35a7ae1 100644 --- a/cbz/page.py +++ b/cbz/page.py @@ -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: + super(PageInfo, self).__init__(kwargs) + self.content = content + + @property + def content(self) -> bytes: + """ + content getter + :return: + """ + 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.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) - } + 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,11 @@ 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() diff --git a/cbz/utils.py b/cbz/utils.py index ee7dbcd..71d7038 100644 --- a/cbz/utils.py +++ b/cbz/utils.py @@ -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 From 9549a375a32ed81bff8dff53ebbef0cc181b368d Mon Sep 17 00:00:00 2001 From: Oleksii Pyskun Date: Tue, 2 Jul 2024 12:11:25 +0300 Subject: [PATCH 3/5] fix info --- cbz/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbz/page.py b/cbz/page.py index 35a7ae1..5410e21 100644 --- a/cbz/page.py +++ b/cbz/page.py @@ -22,7 +22,7 @@ class PageInfo(PageModel): def content(self) -> bytes: """ content getter - :return: + :return: image content """ return self._content From bdda6864d4dd249ba87a48fc29f40dc0a762e774 Mon Sep 17 00:00:00 2001 From: Oleskii Pyskun Date: Tue, 2 Jul 2024 12:44:01 +0300 Subject: [PATCH 4/5] fix info --- cbz/page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbz/page.py b/cbz/page.py index 35a7ae1..5410e21 100644 --- a/cbz/page.py +++ b/cbz/page.py @@ -22,7 +22,7 @@ class PageInfo(PageModel): def content(self) -> bytes: """ content getter - :return: + :return: image content """ return self._content From 9ef572f05eb385eb8f196545811ba3e24315d1e0 Mon Sep 17 00:00:00 2001 From: Oleskii Pyskun Date: Tue, 2 Jul 2024 12:58:31 +0300 Subject: [PATCH 5/5] Implementation save to file page --- cbz/comic.py | 9 +++++++++ cbz/page.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/cbz/comic.py b/cbz/comic.py index e840c36..092307a 100644 --- a/cbz/comic.py +++ b/cbz/comic.py @@ -175,3 +175,12 @@ class ComicInfo(ComicModel): :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) diff --git a/cbz/page.py b/cbz/page.py index 5410e21..29e0beb 100644 --- a/cbz/page.py +++ b/cbz/page.py @@ -68,3 +68,12 @@ class PageInfo(PageModel): """ 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)