diff --git a/inbac/controller.py b/inbac/controller.py index 77486f1..2e0b19c 100644 --- a/inbac/controller.py +++ b/inbac/controller.py @@ -10,7 +10,6 @@ from inbac.model import Model from inbac.view import View - class Controller(): def __init__(self, model: Model, view: View): self.model: Model = model @@ -60,9 +59,11 @@ def load_images(self): except IOError: self.next_image() - def display_image_on_canvas(self, image: Image): + def display_image_on_canvas(self, image: Image.Image): self.clear_canvas() self.model.current_image = image + if self.model.current_image is None: + return self.model.canvas_image_scaling_ratio = self.calculate_canvas_image_scaling( self.model.current_image.size[0], self.model.current_image.size[1], @@ -76,9 +77,9 @@ def display_image_on_canvas(self, image: Image): self.model.current_image.size[0], self.model.current_image.size[1], self.model.canvas_image_scaling_ratio) - displayed_image: Image = self.model.current_image.copy() + displayed_image: Image.Image = self.model.current_image.copy() displayed_image.thumbnail( - self.model.canvas_image_dimensions, Image.ANTIALIAS) + self.model.canvas_image_dimensions, Image.Resampling.LANCZOS) self.model.displayed_image = ImageTk.PhotoImage(displayed_image) self.model.canvas_image = self.view.display_image( self.model.displayed_image) @@ -96,6 +97,7 @@ def clear_selection_box(self): def update_selection_box(self): selected_box: Tuple[int, int, int, int] = self.get_selected_box( + self.model.selected_fixed_size, self.model.canvas_image_scaling_ratio, self.model.press_coord, self.model.move_coord, self.model.args.aspect_ratio) if self.model.selection_box is None: @@ -156,23 +158,23 @@ def save_next(self): self.next_image() def save(self) -> bool: - if self.model.selection_box is None: + if self.model.selection_box is None or self.model.current_image is None: return False selected_box: Tuple[int, int, int, int] = self.view.get_canvas_object_coords( self.model.selection_box) box: Tuple[int, int, int, int] = self.get_real_box( selected_box, self.model.current_image.size, self.model.canvas_image_dimensions) if self.model.selected_fixed_size is not None: - box = [box[0], + box = (box[0], box[1], box[0] + self.model.selected_fixed_size[0], - box[1] + self.model.selected_fixed_size[1]] + box[1] + self.model.selected_fixed_size[1]) new_filename: str = self.find_available_name( self.model.args.output_dir, self.model.images[self.model.current_file]) - saved_image: Image = self.model.current_image.copy().crop(box) + saved_image: Image.Image = self.model.current_image.copy().crop(box) if self.model.args.resize: saved_image = saved_image.resize( - (self.model.args.resize[0], self.model.args.resize[1]), Image.LANCZOS) + (self.model.args.resize[0], self.model.args.resize[1]), Image.Resampling.LANCZOS) if self.model.args.image_format: new_filename, _ = os.path.splitext(new_filename) if not os.path.exists(self.model.args.output_dir): @@ -198,7 +200,7 @@ def save(self) -> bool: def rotate_image(self): if self.model.current_image is not None: - rotated_image = self.model.current_image.transpose(Image.ROTATE_90) + rotated_image = self.model.current_image.transpose(Image.Transpose.ROTATE_90) self.model.current_image.close() self.model.current_image = None self.display_image_on_canvas(rotated_image) @@ -241,7 +243,7 @@ def load_image_list(directory: str) -> List[str]: @staticmethod def coordinates_in_selection_box( - coordinates: Tuple[int, int], selection_box: Tuple[int, int]) -> bool: + coordinates: Tuple[int, int], selection_box: Tuple[int, int, int, int]) -> bool: return (coordinates[0] > selection_box[0] and coordinates[0] < selection_box[2] and coordinates[1] > selection_box[1] and coordinates[1] < selection_box[3]) @@ -258,6 +260,7 @@ def find_available_name(directory: str, filename: str) -> str: str(num) + extension)): return name + str(num) + extension + raise ValueError("No available name found") @staticmethod def get_selection_box_for_aspect_ratio(selection_box: Tuple[int, @@ -272,24 +275,26 @@ def get_selection_box_for_aspect_ratio(selection_box: Tuple[int, int, int, int]: - selection_box: List[int] = list(selection_box) width: int = selection_box[2] - selection_box[0] height: int = selection_box[3] - selection_box[1] if float(width) / float(height) > aspect_ratio: height = round(width / aspect_ratio) if mouse_move_coord[1] > mouse_press_coord[1]: - selection_box[3] = selection_box[1] + height + return (selection_box[0], selection_box[1], selection_box[2], selection_box[1] + height) else: - selection_box[1] = selection_box[3] - height + return (selection_box[0], selection_box[3] - height, selection_box[2], selection_box[3]) else: width = round(height * aspect_ratio) if mouse_move_coord[0] > mouse_press_coord[0]: - selection_box[2] = selection_box[0] + width + return (selection_box[0], selection_box[1], selection_box[0] + width, selection_box[3]) else: - selection_box[0] = selection_box[2] - width - return tuple(selection_box) + return (selection_box[2] - width, selection_box[1], selection_box[2], selection_box[3]) - def get_selected_box(self, + + @staticmethod + def get_selected_box(selected_fixed_size: Optional[Tuple[int, + int]], + canvas_image_scaling_ratio: Optional[float], mouse_press_coord: Tuple[int, int], mouse_move_coord: Tuple[int, @@ -300,10 +305,10 @@ def get_selected_box(self, int, int]: - if self.model.selected_fixed_size is not None: + if selected_fixed_size is not None and canvas_image_scaling_ratio is not None: mouse_press_coord = mouse_move_coord - mouse_move_coord = (mouse_press_coord[0] + self.model.selected_fixed_size[0] * self.model.canvas_image_scaling_ratio, - mouse_press_coord[1] + self.model.selected_fixed_size[1] * self.model.canvas_image_scaling_ratio) + mouse_move_coord = (int(mouse_press_coord[0] + selected_fixed_size[0] * canvas_image_scaling_ratio), + int(mouse_press_coord[1] + selected_fixed_size[1] * canvas_image_scaling_ratio)) selection_top_left_x: int = min( @@ -323,11 +328,9 @@ def get_selected_box(self, selection_bottom_right_y) if aspect_ratio is not None: - aspect_ratio: float = float( - aspect_ratio[0]) / float(aspect_ratio[1]) try: selection_box: Tuple[int, int, int, int] = Controller.get_selection_box_for_aspect_ratio( - selection_box, aspect_ratio, mouse_press_coord, mouse_move_coord) + selection_box, float(aspect_ratio[0]) / float(aspect_ratio[1]), mouse_press_coord, mouse_move_coord) except ZeroDivisionError: pass diff --git a/inbac/inbac.py b/inbac/inbac.py index cbd94ca..ccf7539 100644 --- a/inbac/inbac.py +++ b/inbac/inbac.py @@ -17,6 +17,8 @@ def __init__(self, args: Namespace, master: Tk): if args.input_dir is None: args.input_dir = filedialog.askdirectory(parent=master) + if args.input_dir == () or args.input_dir == "" or args.input_dir is None: + raise ValueError("No input directory specified") args.output_dir = getattr( args, "output_dir", os.path.join(args.input_dir, "crops")) diff --git a/inbac/model.py b/inbac/model.py index db9a7cd..8ac6811 100644 --- a/inbac/model.py +++ b/inbac/model.py @@ -13,9 +13,9 @@ def __init__(self, args): self.move_coord: Tuple[int, int] = (0, 0) self.displayed_image: Optional[PhotoImage] = None self.canvas_image: Optional[Any] = None - self.cavnas_image_scaling_ratio: Optional[float] = None + self.canvas_image_scaling_ratio: Optional[float] = None self.canvas_image_dimensions: Tuple[int, int] = (0, 0) - self.current_image: Optional[Image] = None + self.current_image: Optional[Image.Image] = None self.enabled_selection_mode: bool = False self.box_selected: bool = False self.current_file: int = 0 diff --git a/inbac/view.py b/inbac/view.py index 01635ca..bef848c 100644 --- a/inbac/view.py +++ b/inbac/view.py @@ -1,10 +1,12 @@ import tkinter as tk import types from tkinter import Tk, Frame, Canvas, Event, Menu, messagebox, filedialog, Toplevel -from typing import Tuple, Any +from typing import Tuple, Any, Optional from PIL.ImageTk import PhotoImage import inbac - +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from inbac.controller import Controller class View(): def __init__(self, master: Tk, initial_window_size: Tuple[int, int]): @@ -16,7 +18,7 @@ def __init__(self, master: Tk, initial_window_size: Tuple[int, int]): self.master.geometry( str(initial_window_size[0]) + "x" + str(initial_window_size[1])) self.master.update() - self.controller = None + self.controller: Optional['Controller'] = None self.bind_events() self.create_menu() @@ -56,10 +58,13 @@ def ask_directory(self) -> str: return filedialog.askdirectory(parent=self.master) def open_dialog(self): - self.controller.select_images_folder() - self.controller.load_images() + if self.controller is not None: + self.controller.select_images_folder() + self.controller.load_images() def create_settings_window(self): + if self.controller is None: + return settings_window = tk.Toplevel(self.master) settings_window.title("Settings") settings_window.geometry("{}x{}".format(400, 400)) @@ -149,7 +154,7 @@ def create_settings_window(self): settings.selection_box_color_choices = [ "black", "white", "red", "green", "blue", "cyan", "yellow", "magenta"] settings.selection_box_color_listbox = tk.Listbox( - settings_window, listvariable=tk.StringVar( + settings_window, listvariable=tk.ListVar( # type: ignore value=settings.selection_box_color_choices)) if self.controller.model.args.selection_box_color in settings.selection_box_color_choices: selection_box_color_index = settings.selection_box_color_choices.index( @@ -176,6 +181,8 @@ def create_settings_window(self): def save_settings(self, settings_window: Toplevel, settings: types.SimpleNamespace): + if self.controller is None: + return if settings.aspect_ratio_checked.get(): self.controller.model.args.aspect_ratio = ( int(settings.aspect_ratio_x.get()), int(settings.aspect_ratio_y.get())) @@ -213,7 +220,7 @@ def create_rectangle( self, box: Tuple[int, int, int, int], outline_color: str) -> Any: return self.image_canvas.create_rectangle(box, outline=outline_color) - def change_canvas_object_coords(self, obj: Any, coords: Tuple[int, int]): + def change_canvas_object_coords(self, obj: Any, coords: Tuple[int, int, int, int]): self.image_canvas.coords(obj, coords) def get_canvas_object_coords(self, obj: Any) -> Any: @@ -226,58 +233,71 @@ def move_canvas_object_by_offset( offset_y: int): self.image_canvas.move(obj, offset_x, offset_y) - def enable_selection_mode(self, event: Event = None): - self.controller.model.enabled_selection_mode = True + def enable_selection_mode(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.model.enabled_selection_mode = True - def disable_selection_mode(self, event: Event = None): - self.controller.model.enabled_selection_mode = False + def disable_selection_mode(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.model.enabled_selection_mode = False def on_mouse_down(self, event: Event): - self.controller.start_selection((event.x, event.y)) + if self.controller is not None: + self.controller.start_selection((event.x, event.y)) def on_mouse_drag(self, event: Event): - self.controller.move_selection((event.x, event.y)) + if self.controller is not None: + self.controller.move_selection((event.x, event.y)) def on_mouse_up(self, event: Event): - self.controller.stop_selection() + if self.controller is not None: + self.controller.stop_selection() - def next_image(self, event: Event = None): - self.controller.next_image() + def next_image(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.next_image() - def previous_image(self, event: Event = None): - self.controller.previous_image() + def previous_image(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.previous_image() - def on_resize(self, event: Event = None): - self.controller.display_image_on_canvas( - self.controller.model.current_image) + def on_resize(self, event: Optional[Event] = None): + if self.controller is not None and self.controller.model.current_image is not None: + self.controller.display_image_on_canvas( + self.controller.model.current_image) - def save_next(self, event: Event = None): - self.controller.save_next() + def save_next(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.save_next() - def save(self, event: Event = None): - self.controller.save() + def save(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.save() def set_title(self, title: str): self.master.title(title) - def rotate_image(self, event: Event = None): - self.controller.rotate_image() - - def rotate_aspect_ratio(self, event: Event = None): - self.controller.rotate_aspect_ratio() - - def cycle_fixed_selection_sizes(self, event: Event = None): - model = self.controller.model - if len(model.args.fixed_sizes) == 0: - return - - if model.selected_fixed_size_index is None: - model.selected_fixed_size_index = 0 - else: - model.selected_fixed_size_index += 1 - - if model.selected_fixed_size_index == len(model.args.fixed_sizes): - model.selected_fixed_size_index = None - model.selected_fixed_size = None - else: - model.selected_fixed_size = model.args.fixed_sizes[model.selected_fixed_size_index] + def rotate_image(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.rotate_image() + + def rotate_aspect_ratio(self, event: Optional[Event] = None): + if self.controller is not None: + self.controller.rotate_aspect_ratio() + + def cycle_fixed_selection_sizes(self, event: Optional[Event] = None): + if self.controller is not None: + model = self.controller.model + if len(model.args.fixed_sizes) == 0: + return + + if model.selected_fixed_size_index is None: + model.selected_fixed_size_index = 0 + else: + model.selected_fixed_size_index += 1 + + if model.selected_fixed_size_index == len(model.args.fixed_sizes): + model.selected_fixed_size_index = None + model.selected_fixed_size = None + else: + model.selected_fixed_size = model.args.fixed_sizes[model.selected_fixed_size_index] diff --git a/tests/test_inbac.py b/tests/test_inbac.py index d382e1d..5b95710 100644 --- a/tests/test_inbac.py +++ b/tests/test_inbac.py @@ -37,29 +37,27 @@ def test_find_available_name_returns_name_with_number_if_file_exists(self, mock_ self.assertEqual(new_filename, returned_filename) def test_selection_box_for_aspect_ratio_returns_box_with_aspect_ratio(self): - aspect_ratio = 16.0/9.0 - mouse_press_coord = (0.0, 0.0) - mouse_move_coord = (15.0, 9.0) - selection_box = (0.0, 0.0, 15.0, 9.0) - expected_selection_box = (0.0, 0.0, 16.0, 9.0) + aspect_ratio = 16 / 9 + mouse_press_coord = (0, 0) + mouse_move_coord = (15, 9) + selection_box = (0, 0, 15, 9) + expected_selection_box = (0, 0, 16, 9) returned_selection_box = Controller.get_selection_box_for_aspect_ratio(selection_box, aspect_ratio, mouse_press_coord, mouse_move_coord) self.assertEqual(expected_selection_box, returned_selection_box) def test_get_selected_box_returns_correct_selection_box_when_selecting_from_upper_left_to_bottom_right(self): - mouse_press_coord = (0.0, 0.0) - mouse_move_coord = (15.0, 9.0) - expected_selection_box = (0.0, 0.0, 15.0, 9.0) - returned_selection_box = Controller.get_selected_box( - mouse_press_coord, mouse_move_coord, None) + mouse_press_coord = (0, 0) + mouse_move_coord = (15, 9) + expected_selection_box = (0, 0, 15, 9) + returned_selection_box = Controller.get_selected_box(None, None, mouse_press_coord, mouse_move_coord, None) self.assertEqual(expected_selection_box, returned_selection_box) def test_get_selected_box_returns_correct_selection_box_when_selecting_from_bottom_right_to_upper_left(self): - mouse_press_coord = (15.0, 9.0) - mouse_move_coord = (0.0, 0.0) - expected_selection_box = (0.0, 0.0, 15.0, 9.0) - returned_selection_box = Controller.get_selected_box( - mouse_press_coord, mouse_move_coord, None) + mouse_press_coord = (15, 9) + mouse_move_coord = (0, 0) + expected_selection_box = (0, 0, 15, 9) + returned_selection_box = Controller.get_selected_box(None, None, mouse_press_coord, mouse_move_coord, None) self.assertEqual(expected_selection_box, returned_selection_box) def test_get_real_box(self): @@ -151,10 +149,8 @@ def test_previous_image_first_image(self, mock_view): def test_calculate_canvas_image_dimensions(self): image_width = 1000 image_height = 500 - canvas_width = 500 - canvas_height = 400 - (new_width, new_height) = Controller.calculate_canvas_image_dimensions(image_width, image_height, - canvas_width, canvas_height) + scaling_ratio = 0.5 + (new_width, new_height) = Controller.calculate_canvas_image_dimensions(image_width, image_height, scaling_ratio) self.assertEqual(image_width / 2, new_width) self.assertEqual(image_height / 2, new_height)