diff --git a/croppa/main.py b/croppa/main.py index 1b2eca6..6df261f 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -46,6 +46,9 @@ class VideoEditor: # Supported video extensions VIDEO_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv", ".webm", ".m4v"} + + # Crop adjustment settings + CROP_SIZE_STEP = 15 # pixels to expand/contract crop def __init__(self, path: str): self.path = Path(path) @@ -112,6 +115,9 @@ class VideoEditor: self.progress_bar_complete_time = None self.progress_bar_text = "" self.progress_bar_fps = 0.0 # Current rendering FPS + + # Crop adjustment settings + self.crop_size_step = self.CROP_SIZE_STEP def _get_next_edited_filename(self, video_path: Path) -> str: """Generate the next available _edited_%03d filename""" @@ -641,12 +647,8 @@ class VideoEditor: def draw_crop_overlay(self, canvas, start_x, start_y, frame_width, frame_height): """Draw crop overlay on canvas using screen coordinates""" - # Draw preview rectangle (green) - already in screen coordinates - if self.crop_preview_rect: - x, y, w, h = self.crop_preview_rect - cv2.rectangle( - canvas, (int(x), int(y)), (int(x + w), int(y + h)), (0, 255, 0), 2 - ) + # Note: crop_preview_rect disabled as it was showing in wrong position + # The final crop rectangle (red) is sufficient for visual feedback # Draw final crop rectangle (red) - convert from video to screen coordinates if self.crop_rect: @@ -978,6 +980,78 @@ class VideoEditor: else: self.crop_rect = None + + + def adjust_crop_size(self, direction: str, expand: bool, amount: int = None): + """ + Adjust crop size in given direction + direction: 'up', 'down', 'left', 'right' + expand: True to expand, False to contract + amount: pixels to adjust by (uses self.crop_size_step if None) + """ + if amount is None: + amount = self.crop_size_step + if not self.crop_rect: + # If no crop exists, create a default one in the center + center_x = self.frame_width // 2 + center_y = self.frame_height // 2 + default_size = min(self.frame_width, self.frame_height) // 4 + self.crop_rect = ( + center_x - default_size // 2, + center_y - default_size // 2, + default_size, + default_size + ) + return + + x, y, w, h = self.crop_rect + + if direction == 'up': + if expand: + # Expand upward - decrease y, increase height + new_y = max(0, y - amount) + new_h = h + (y - new_y) + self.crop_rect = (x, new_y, w, new_h) + else: + # Contract from bottom - decrease height + new_h = max(10, h - amount) # Minimum size of 10 pixels + self.crop_rect = (x, y, w, new_h) + + elif direction == 'down': + if expand: + # Expand downward - increase height + new_h = min(self.frame_height - y, h + amount) + self.crop_rect = (x, y, w, new_h) + else: + # Contract from top - increase y, decrease height + amount = min(amount, h - 10) # Don't make it smaller than 10 pixels + new_y = y + amount + new_h = h - amount + self.crop_rect = (x, new_y, w, new_h) + + elif direction == 'left': + if expand: + # Expand leftward - decrease x, increase width + new_x = max(0, x - amount) + new_w = w + (x - new_x) + self.crop_rect = (new_x, y, new_w, h) + else: + # Contract from right - decrease width + new_w = max(10, w - amount) # Minimum size of 10 pixels + self.crop_rect = (x, y, new_w, h) + + elif direction == 'right': + if expand: + # Expand rightward - increase width + new_w = min(self.frame_width - x, w + amount) + self.crop_rect = (x, y, new_w, h) + else: + # Contract from left - increase x, decrease width + amount = min(amount, w - 10) # Don't make it smaller than 10 pixels + new_x = x + amount + new_w = w - amount + self.crop_rect = (new_x, y, new_w, h) + def render_video(self, output_path: str): """Optimized video rendering with multithreading and batch processing""" if not output_path.endswith(".mp4"): @@ -1179,9 +1253,16 @@ class VideoEditor: print(" E/Shift+E: Increase/Decrease brightness") print(" R/Shift+R: Increase/Decrease contrast") print(" -: Rotate clockwise 90°") + print() + print("Crop Controls:") print(" Shift+Click+Drag: Select crop area") + print(" I/J/K/L: Contract crop (up/left/down/right)") + print(" Shift+I/J/K/L: Expand crop (up/left/down/right)") + print(" [/]: Contract/Expand crop (cycles directions)") print(" U: Undo crop") print(" C: Clear crop") + print() + print("Other Controls:") print(" Ctrl+Scroll: Zoom in/out") print(" 1: Set cut start point") print(" 2: Set cut end point") @@ -1279,6 +1360,59 @@ class VideoEditor: elif key == 13: # Enter output_name = self._get_next_edited_filename(self.video_path) self.render_video(str(self.video_path.parent / output_name)) + + + + # Use keyboard shortcuts for crop size adjustment since modifier detection is unreliable + # Using bracket keys for crop size adjustment + elif key == ord("["): # Contract crop + # Cycle through directions: up, right, down, left + if not hasattr(self, '_contract_direction'): + self._contract_direction = 0 + directions = ['up', 'right', 'down', 'left'] + direction = directions[self._contract_direction % 4] + self.adjust_crop_size(direction, False, 20) + self._contract_direction += 1 + print(f"Contracted crop {direction}") + elif key == ord("]"): # Expand crop + # Cycle through directions: up, right, down, left + if not hasattr(self, '_expand_direction'): + self._expand_direction = 0 + directions = ['up', 'right', 'down', 'left'] + direction = directions[self._expand_direction % 4] + self.adjust_crop_size(direction, True, 20) + self._expand_direction += 1 + print(f"Expanded crop {direction}") + + # Individual direction controls using shift combinations we can detect + elif key == ord("I"): # Shift+i - expand up + self.adjust_crop_size('up', True) + print(f"Expanded crop upward by {self.crop_size_step}px") + elif key == ord("K"): # Shift+k - expand down + self.adjust_crop_size('down', True) + print(f"Expanded crop downward by {self.crop_size_step}px") + elif key == ord("J"): # Shift+j - expand left + self.adjust_crop_size('left', True) + print(f"Expanded crop leftward by {self.crop_size_step}px") + elif key == ord("L"): # Shift+l - expand right + self.adjust_crop_size('right', True) + print(f"Expanded crop rightward by {self.crop_size_step}px") + + # Contract in specific directions + elif key == ord("i"): # i - contract from bottom (reduce height from bottom) + self.adjust_crop_size('up', False) + print(f"Contracted crop from bottom by {self.crop_size_step}px") + elif key == ord("k"): # k - contract from top (reduce height from top) + self.adjust_crop_size('down', False) + print(f"Contracted crop from top by {self.crop_size_step}px") + elif key == ord("j"): # j - contract from right (reduce width from right) + self.adjust_crop_size('left', False) + print(f"Contracted crop from right by {self.crop_size_step}px") + elif key == ord("l"): # l - contract from left (reduce width from left) + self.adjust_crop_size('right', False) + print(f"Contracted crop from left by {self.crop_size_step}px") + + # Auto advance frame when playing if self.is_playing: