diff --git a/README.md b/README.md index 3326efd..c840388 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ OCR-Manga has a few dependencies you will need: in their package repositories. - [pillow](https://github.com/python-pillow/Pillow) - [pyocr](https://github.com/jflesch/pyocr) +- [ConfigObj](https://github.com/DiffSK/configobj) - [magic](https://github.com/ahupp/python-magic) - [myougiden](https://github.com/leoboiko/myougiden) - [rarfile](https://github.com/markokr/rarfile) @@ -22,7 +23,7 @@ Ubuntu/Debian Install pip, Tk, and Tesseract: -`sudo apt-get install python3-pip python3-tk tesseract-ocr tesseract-ocr-jpn` +`sudo apt-get install python3-pip python3-configobj python3-tk tesseract-ocr tesseract-ocr-jpn` Arch Linux @@ -30,7 +31,7 @@ Arch Linux Install pip, Tk, and Tesseract: -`sudo pacman -S python-pip tk tesseract tesseract-data-jpn` +`sudo pacman -S python-pip python-configobj tk tesseract tesseract-data-jpn` Gentoo @@ -41,7 +42,7 @@ either have `ja` in your L10N or specify `l10n_ja` as an USE flag for Tesseract. Install pip and Tesseract: -`sudo emerge -a dev-python/pip app-lang/tesseract` +`sudo emerge -a dev-python/pip dev-python/configobj app-lang/tesseract` Install various python modules: ------------------------------- @@ -54,6 +55,9 @@ Install various python modules: The manga can be a zip, rar, tar, or just a plain old directory. +OCR-Manga accepts a configuration file in ``~/.config/OCR-Manga/config``. +See the [options](https://github.com/Dudemanguy911/OCR-Manga/blob/master/options.md) page for a list of default options and their key bindings. + ## Contributions Contributions are welcomed and accepted. It is required that all pull diff --git a/Reader.py b/Reader.py index 09a9369..fa9f011 100755 --- a/Reader.py +++ b/Reader.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 import argparse +from configobj import ConfigObj +from validate import Validator +import io import json from multiprocessing import Process, Queue import os @@ -25,6 +28,32 @@ '33': '#cdcd00', '35': '#cd00cd', '36': '#00cdcd'} +default_opts = io.StringIO( + 'double_page=boolean(default=False)\n' + 'fit_mode=option(best, height, width, default="best")\n' + 'fullscreen=boolean(default=False)\n' + 'height=integer(default=500)\n' + 'jump_to_beginning=string(default="")\n' + 'jump_to_end=string(default="")\n' + 'manga_mode=boolean(default=True)\n' + 'next_image=string(default="")\n' + 'prev_image=string(default="")\n' + 'rotate=string(default="")\n' + 'rotation=integer(0, 3, default=0)\n' + 'scale_method=option(nearest, bilinear, bicubic, lanczos, default="lanczos")\n' + 'set_best_fit=string(default="")\n' + 'set_height_fit=string(default="")\n' + 'set_width_fit=string(default="")\n' + 'single_step_backward=string(default="")\n' + 'single_step_forward=string(default="")\n' + 'toggle_double_page=string(default="")\n' + 'toggle_fullscreen=string(default="")\n' + 'toggle_manga_mode=string(default="")\n' + 'width=integer(default=500)') +config = ConfigObj(os.path.expanduser('~/.config/OCR-Manga/config'), + list_values=False, + configspec=default_opts) +config.validate(Validator()) class Application(tk.Frame): @@ -52,58 +81,123 @@ def __init__(self, images, master=None): self.current_page = 0 self.pack(fill=tk.BOTH, expand=1) - self.createWidgets() self.current_page_oid = 0 self.current_page_image = None self.current_page_file = None + self.second_page_oid = 0 + self.second_page_image = None + self.second_page_file = None self.drawing_box = False self.box_oid = 0 self.box_coords = (0, 0, 0, 0) self.lookup = None self.tkimage = None - self.rotation = 0 - self.fullscreen = False + self.rotation = config.as_int('rotation') + self.double_page = config.as_bool('double_page') + self.fit_mode = config.get('fit_mode') + self.fullscreen = config.as_bool('fullscreen') + self.master.attributes("-fullscreen", self.fullscreen) + self.manga_mode = config.as_bool('manga_mode') + self.width_valid = None + self.createWidgets() self.text = [] self.draw_queue = Queue() self.after(100, self.check_queue) - def best_fit(self, width, height, image): - (x, y) = image.size - scale = width / x - if y * scale > height: - scale = height / y - # print(scale) - new_x = int(x * scale) - new_y = int(y * scale) - if new_x <= 0: - new_x = 1 - if new_y <= 0: - new_y = 1 - return image.resize((new_x, new_y), Image.BILINEAR) + def anchor_select(self, i): + anchors = [tk.W, tk.N, tk.E, tk.S] + select = (self.rotation + i) % 4 + return anchors[select] def change_image(self, amount): self.kill_lookup() new_page = self.current_page + amount + double_page = self.double_page if new_page < 0 or new_page > len(self.image_files) - 1: return - self.clear_box() + if double_page and new_page > len(self.image_files) - 2: + double_page = False self.current_page = new_page - self.master.title("Yurumon reader (%d/%d)" % (new_page + 1, - len(self.image_files))) + self.clear_box() if self.current_page_file is not None: self.current_page_file.close() self.current_page_file = self.images.open(self.image_files[new_page]) image = Image.open(self.current_page_file) - if self.rotation != 0: - image = image.rotate(-90 * self.rotation) (width, height) = (self.frame.winfo_width(), self.frame.winfo_height()) - image = self.best_fit(width, height, image) - self.tkimage = ImageTk.PhotoImage(image) + fit_mode = self.fit_mode self.frame.delete(self.current_page_oid) - self.current_page_oid = self.frame.create_image(int(width/2), - int(height/2), - image=self.tkimage) - self.current_page_image = image + self.frame.delete(self.second_page_oid) + + if double_page: + second_page = self.current_page + 1 + self.second_page_file = self.images.open(self.image_files[second_page]) + second_image = Image.open(self.second_page_file) + # check image widths, resize, and display them + if width < image.width + second_image.width: + if image.width > image.height or second_image.width > second_image.height: + self.width_valid = False + image = self.fit_mode_select(width, height, image, mode=fit_mode) + second_image = self.fit_mode_select(width, height, second_image, + mode=fit_mode) + else: + image = self.fit_mode_select(width, height, image, + mode=fit_mode) + second_image = self.fit_mode_select(width, height, second_image, + mode=fit_mode) + if width < image.width + second_image.width: + if self.rotation % 2 == 0: + self.width_valid = False + else: + self.width_valid = True + else: + self.width_valid = True + else: + self.width_valid = True + image = self.fit_mode_select(width, height, image, mode=fit_mode) + second_image = self.fit_mode_select(width, height, second_image, mode=fit_mode) + self.tkimage = ImageTk.PhotoImage(image) + self.tksecond_image = ImageTk.PhotoImage(second_image) + self.current_page_image = image + self.second_page_image = second_image + else: + image = self.fit_mode_select(width, height, image, mode=fit_mode) + self.tkimage = ImageTk.PhotoImage(image) + self.current_page_image = image + + if not self.width_valid and amount < 0: + self.master.title("Yurumon reader (%d/%d)" % (new_page + 2, + len(self.image_files))) + elif not double_page or (not self.width_valid and amount >= 0): + self.master.title("Yurumon reader (%d/%d)" % (new_page + 1, + len(self.image_files))) + else: + self.master.title("Yurumon reader (%d-%d/%d)" % (new_page + 1, + new_page + 2, + len(self.image_files))) + if self.manga_mode: + left_anchor = self.anchor_select(2) + right_anchor = self.anchor_select(0) + else: + left_anchor = self.anchor_select(0) + right_anchor = self.anchor_select(2) + + if not self.width_valid and amount < 0: + self.current_page_oid = self.frame.create_image(int(width/2), + int(height/2), + image=self.tksecond_image) + elif not double_page or (not self.width_valid and amount >= 0): + self.current_page_oid = self.frame.create_image(int(width/2), + int(height/2), + image=self.tkimage) + else: + self.current_page_oid = self.frame.create_image(int(width/2), + int(height/2), + anchor=right_anchor, + image=self.tkimage) + self.second_page_oid = self.frame.create_image(int(width/2), + int(height/2), + anchor=left_anchor, + image=self.tksecond_image) last_page = open("last_page", "w") self.last_page_json[self.images.path] = new_page json.dump(self.last_page_json, last_page) @@ -131,22 +225,37 @@ def createWidgets(self): # command=self.quit) # self.quitButton.grid() self.update() - self.frame = tk.Canvas(self, width=500, - height=500, cursor="tcross", + self.frame = tk.Canvas(self, width=config.as_int('width'), + height=config.as_int('height'), cursor="tcross", background="black", highlightthickness=0) self.frame.pack(fill=tk.BOTH, expand=1) - self.frame.bind('', self.next_image) - self.frame.bind('', self.prev_image) - # self.frame.bind('', self.rotate) - self.frame.bind('', self.resize_event) + if self.manga_mode: + self.frame.bind(config.get('next_image'), self.next_image) + self.frame.bind(config.get('prev_image'), self.prev_image) + self.frame.bind(config.get('single_step_backward'), self.single_step_backward) + self.frame.bind(config.get('single_step_forward'), self.single_step_forward) + else: + self.frame.bind(config.get('next_image'), self.prev_image) + self.frame.bind(config.get('prev_image'), self.next_image) + self.frame.bind(config.get('single_step_backward'), self.single_step_forward) + self.frame.bind(config.get('single_step_forward'), self.single_step_backward) + self.frame.bind(config.get('jump_to_beginning'), self.jump_to_beginning) + self.frame.bind(config.get('jump_to_end'), self.jump_to_end) + self.frame.bind(config.get('rotate'), self.rotate) + self.frame.bind(config.get('set_best_fit'), self.set_best_fit) + self.frame.bind(config.get('set_height_fit'), self.set_height_fit) + self.frame.bind(config.get('set_width_fit'), self.set_width_fit) + self.frame.bind(config.get('toggle_double_page'), self.toggle_double_page) + self.frame.bind(config.get('toggle_manga_mode'), self.toggle_manga_mode) self.frame.focus_set() + self.frame.bind('', self.resize_event) self.frame.bind('', self.start_drawing_box) self.frame.bind('', self.stop_drawing_box) self.frame.bind('', self.clear_box) self.frame.bind('', self.side_tap) self.frame.bind('', self.side_tap) self.frame.bind('', self.draw_box) - self.frame.bind('', self.toggle_fullscreen) + self.frame.bind(config.get('toggle_fullscreen'), self.toggle_fullscreen) def draw(self, string): self.draw_queue.put(string) @@ -194,11 +303,90 @@ def draw_dict(self, string): self.frame.addtag_withtag("text", self.textbox) self.frame.tag_lower(self.textbox, self.text[0]) + def fit_mode_select(self, width, height, image, mode=None): + if mode is None: + mode = config.get('fit_mode') + if mode == 'best': + return self.fit_to_best(width, height, image) + if mode == 'height': + return self.fit_to_height(width, height, image) + if mode == 'width': + return self.fit_to_width(width, height, image) + + def fit_to_best(self, width, height, image): + scale_method = self.scale_select() + if self.rotation != 0: + image = image.rotate(-90 * self.rotation, scale_method, expand=1) + (x, y) = image.size + if not self.double_page: + scale = width / x + if y * scale > height: + scale = height / y + elif self.width_valid: + if self.rotation % 2 == 0: + scale = (width/2) / x + if y * scale > height: + scale = height / y + else: + scale = (height/2) / y + if x * scale > width: + scale = width / y + else: + scale = width / x + if y * scale > height: + scale = height / y + new_x = int(x * scale) + new_y = int(y * scale) + if new_x <= 0: + new_x = 1 + if new_y <= 0: + new_y = 1 + return image.resize((new_x, new_y), scale_method) + + def fit_to_height(self, width, height, image): + scale_method = self.scale_select() + if self.rotation != 0: + image = image.rotate(-90 * self.rotation, scale_method, expand=1) + (x, y) = image.size + if not self.double_page: + scale = height / y + elif self.width_valid: + if self.rotation % 2 == 0: + scale = height / y + else: + scale = (height/2) / y + else: + scale = height / y + new_y = int(y * scale) + if new_y <= 0: + new_y = 1 + return image.resize((x, new_y), scale_method) + + def fit_to_width(self, width, height, image): + scale_method = self.scale_select() + if self.rotation != 0: + image = image.rotate(-90 * self.rotation, scale_method, expand=1) + (x, y) = image.size + if not self.double_page: + scale = width / x + elif self.width_valid: + if self.rotation % 2 == 0: + scale = (width/2) / x + else: + scale = width / x + else: + scale = width / x + new_x = int(x * scale) + if new_x <= 0: + new_x = 1 + return image.resize((new_x, y), scale_method) + def image_to_dict(self, image): bid = self.box_oid mode = 5 size = image.size - image = image.resize((size[0] * 3, size[1] * 3), Image.BICUBIC) + scale_method = self.scale_select() + image = image.resize((size[0] * 3, size[1] * 3), scale_method) if size[0] / size[1] < 1.15 and size[1] / size[0] < 1.15: mode = 10 if size[0] > size[1] * 1.5: @@ -224,6 +412,20 @@ def image_to_dict(self, image): def image_to_string(self, image, lang="jpn", builder=None): return tool.image_to_string(image, lang=lang, builder=builder) + def jump_to_beginning(self, event): + self.current_page = 0 + self.update_screen() + + def jump_to_end(self, event): + if self.double_page: + self.current_page = len(self.image_files) - 2 + else: + self.current_page = len(self.image_files) - 1 + self.update_screen() + if not self.width_valid and self.double_page: + self.current_page = len(self.image_files) - 1 + self.update_screen() + def kill_lookup(self): if self.lookup is not None and self.lookup.is_alive(): try: @@ -239,7 +441,14 @@ def lookup_entry(self, image, coords): self.draw(string) def next_image(self, event): - self.change_image(1) + if (self.current_page == len(self.image_files) - 3) and self.width_valid: + self.change_image(1) + elif not self.width_valid: + self.change_image(1) + elif not self.double_page: + self.change_image(1) + else: + self.change_image(2) def parse_color_string(self, string): escape = "\x1b" @@ -274,7 +483,14 @@ def parse_color_string(self, string): return color_tuples def prev_image(self, event): - self.change_image(-1) + if self.current_page < 2: + self.change_image(-self.current_page) + elif not self.width_valid: + self.change_image(-1) + elif not self.double_page: + self.change_image(-1) + else: + self.change_image(-2) def resize_event(self, event): self.frame.width = event.width # >>>854 @@ -286,12 +502,43 @@ def rotate(self, event): self.rotation = (self.rotation + 1) % 4 self.update_screen() + def scale_select(self): + if config.get('scale_method') == 'nearest': + scale_method = Image.NEAREST + elif config.get('scale_method') == 'bilinear': + scale_method = Image.BILINEAR + elif config.get('scale_method') == 'bicubic': + scale_method = Image.BICUBIC + elif config.get('scale_method') == 'lanczos': + scale_method = Image.LANCZOS + else: + scale_method = Image.LANCZOS + return scale_method + + def set_best_fit(self, event): + self.fit_mode = 'best' + self.update_screen() + + def set_height_fit(self, event): + self.fit_mode = 'height' + self.update_screen() + + def set_width_fit(self, event): + self.fit_mode = 'width' + self.update_screen() + def side_tap(self, event): if event.x < (self.frame.winfo_width() / 2): self.change_image(1) else: self.change_image(-1) + def single_step_backward(self, event): + self.change_image(-1) + + def single_step_forward(self, event): + self.change_image(1) + def start_drawing_box(self, event): self.kill_lookup() textbox = self.frame.bbox("text") @@ -320,6 +567,7 @@ def start_drawing_box(self, event): def stop_drawing_box(self, event): self.drawing_box = False + double_page = self.double_page try: (ix, iy, ix2, iy2) = self.frame.bbox(self.current_page_oid) (bx, by, bx2, by2) = self.frame.bbox(self.box_oid) @@ -327,17 +575,31 @@ def stop_drawing_box(self, event): py = (by - iy) / (iy2 - iy) px2 = (bx2 - ix) / (ix2 - ix) py2 = (by2 - iy) / (iy2 - iy) - # print("%f, %f, %f, %f" % (px, py, px2, py2)) (width, height) = self.current_page_image.size cx = int(px * width) cx2 = int(px2 * width) cy = int(py * height) cy2 = int(py2 * height) - # print("%d, %d, %d, %d" % (cx, cy, cx2, cy2)) - self.lookup = Process(target=self.lookup_entry, - args=(self.current_page_image, - (cx, cy, cx2, cy2))) + if (cx or cx2) > 0: + self.lookup = Process(target=self.lookup_entry, + args=(self.current_page_image, + (cx, cy, cx2, cy2))) + if double_page and ((cx or cx2) < 0): + (jx, jy, jx2, jy2) = self.frame.bbox(self.second_page_oid) + qx = (bx - jx) / (jx2 - jx) + qy = (by - jy) / (jy2 - jy) + qx2 = (bx2 - jx) / (jx2 - jx) + qy2 = (by2 - jy) / (jy2 - jy) + + (width, height) = self.second_page_image.size + dx = int(qx * width) + dx2 = int(qx2 * width) + dy = int(qy * height) + dy2 = int(qy2 * height) + self.lookup = Process(target=self.lookup_entry, + args=(self.second_page_image, + (dx, dy, dx2, dy2))) # draw = ImageDraw.Draw(self.current_page_image) # draw.rectangle([cx, cy, cx2, cy2], outline="black") # ocr_image = image @@ -346,15 +608,32 @@ def stop_drawing_box(self, event): except: pass + def toggle_double_page(self, event): + self.double_page = not self.double_page + self.update_screen() + def toggle_fullscreen(self, event=None): self.fullscreen = not self.fullscreen # Just toggling the boolean self.master.attributes("-fullscreen", self.fullscreen) self.update_screen() + def toggle_manga_mode(self, event): + self.manga_mode = not self.manga_mode + if self.manga_mode: + self.frame.bind(config.get('next_image'), self.next_image) + self.frame.bind(config.get('prev_image'), self.prev_image) + self.frame.bind(config.get('single_step_backward'), self.single_step_backward) + self.frame.bind(config.get('single_step_forward'), self.single_step_forward) + else: + self.frame.bind(config.get('next_image'), self.prev_image) + self.frame.bind(config.get('prev_image'), self.next_image) + self.frame.bind(config.get('single_step_backward'), self.single_step_forward) + self.frame.bind(config.get('single_step_forward'), self.single_step_backward) + self.update_screen() + def update_screen(self): self.change_image(0) - def main(): parser = argparse.ArgumentParser(description="OCR Manga Reader") parser.add_argument('mangafile', metavar='file', help="a .cbz/.zip, " diff --git a/options.md b/options.md new file mode 100644 index 0000000..9562501 --- /dev/null +++ b/options.md @@ -0,0 +1,39 @@ +# Default Options + +All key bindings can be set according to [tkinter](http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm). +Note that `next_image`, `prev_image`, `single_step_backward`, and `single_step_forward` are set with respect to manga mode. + +### Example Configuration + +``` +double_page=True +height=1024 +width=768 +fullscreen_toggle= +``` + +### All Options + +| Name | Description | +| ----------------- | ------------------- | +| double_page | Sets double page mode (default `False`) | +| fit_mode | Sets the fit mode to view images from best, height, and width (default `best`) | +| fullscreen | Sets fullscreen (default `False`) | +| height | Sets window height (default `500`) | +| manga_mode | Sets the image scroll from right to left like a manga (default `True`) | +| jump_to_beginning | Sets key for moving to the first image (default ``) | +| jump_to_end | Sets key for moving to the last image (default ``) | +| next_image | Sets key for moving to the next image (default ``) | +| prev_image | Sets key for moving to the previous image (default ``) | +| rotate | Sets key for rotating image (default ``) | +| rotation | Sets image orientation as an integer multiple of 90 degrees from 0-3 (default `0`) | +| scale_method | Sets image scaling method as nearest, bilinear, bicubic, or lanczos (default `lanczos`) | +| set_best_fit | Sets key to enable best fit mode (default ``) | +| set_height_fit | Sets key to enable height fit mode (default ``) | +| set_width_fith | Sets key to enable width fit mode (default ``) | +| single_step_backward | Sets key to move exactly one page back regardless of double page mode (default ``) | +| single_step_forward | Sets key to move exactly one page forward regardless of double page mode (default ``) | +| toggle_double_page | Sets key for toggling double page mode (default ``) | +| toggle_fullscreen | Sets key for toggling fullscreen (default ``) | +| toggle_manga_mode | Sets key for toggling manga mode (default ``) +| width | Sets window width (default `500`) |