diff --git a/croppa/main.py b/croppa/main.py index fe03f66..d8ddc81 100644 --- a/croppa/main.py +++ b/croppa/main.py @@ -1206,8 +1206,8 @@ class VideoEditor: offy_max = max(0, new_h - self.window_height) offx = max(0, min(int(self.display_offset[0]), offx_max)) offy = max(0, min(int(self.display_offset[1]), offy_max)) - visible_w = self.window_width - visible_h = self.window_height + visible_w = min(new_w, self.window_width) + visible_h = min(new_h, self.window_height) else: offx = 0 offy = 0 @@ -1224,7 +1224,9 @@ class VideoEditor: 'eff_x': eff_x, 'eff_y': eff_y, 'eff_w': eff_w, 'eff_h': eff_h, 'offx': offx, 'offy': offy, 'scale': scale, - 'start_x': start_x, 'start_y': start_y + 'start_x': start_x, 'start_y': start_y, + 'visible_w': visible_w, 'visible_h': visible_h, + 'available_h': available_height } def _map_original_to_screen(self, ox, oy): @@ -2350,21 +2352,10 @@ class VideoEditor: # Send progress update self.render_progress_queue.put(("progress", "Calculating output dimensions...", 0.05, 0.0)) - # Calculate output dimensions (accounting for rotation) - if self.crop_rect: - crop_width = int(self.crop_rect[2]) - crop_height = int(self.crop_rect[3]) - else: - crop_width = self.frame_width - crop_height = self.frame_height - - # Swap dimensions if rotation is 90 or 270 degrees - if self.rotation_angle == 90 or self.rotation_angle == 270: - output_width = int(crop_height * self.zoom_factor) - output_height = int(crop_width * self.zoom_factor) - else: - output_width = int(crop_width * self.zoom_factor) - output_height = int(crop_height * self.zoom_factor) + # Calculate output dimensions to MATCH preview visible region + params = self._get_display_params() + output_width = max(2, params['visible_w'] - (params['visible_w'] % 2)) + output_height = max(2, params['visible_h'] - (params['visible_h'] % 2)) # Ensure dimensions are divisible by 2 for H.264 encoding output_width = output_width - (output_width % 2) @@ -2374,9 +2365,10 @@ class VideoEditor: self.render_progress_queue.put(("progress", "Setting up FFmpeg encoder...", 0.1, 0.0)) # Debug output dimensions - print(f"Output dimensions: {output_width}x{output_height}") + print(f"Output dimensions (match preview): {output_width}x{output_height}") print(f"Zoom factor: {self.zoom_factor}") - print(f"Crop dimensions: {crop_width}x{crop_height}") + eff_x, eff_y, eff_w, eff_h = self._get_effective_crop_rect_for_frame(start_frame) + print(f"Effective crop (rotated): {eff_x},{eff_y} {eff_w}x{eff_h}") # Skip all the OpenCV codec bullshit and go straight to FFmpeg print("Using FFmpeg for encoding with OpenCV transformations...") @@ -2526,32 +2518,45 @@ class VideoEditor: def _process_frame_for_render(self, frame, output_width: int, output_height: int, frame_number: int = None): """Process a single frame for rendering (optimized for speed)""" try: - # Apply crop (vectorized operation) - if self.crop_rect: - if frame_number is None: - x, y, w, h = map(int, self.crop_rect) - else: - x, y, w, h = map(int, self._get_effective_crop_rect_for_frame(frame_number)) + # Apply rotation first to work in rotated space + if self.rotation_angle != 0: + frame = self.apply_rotation(frame) - # Clamp coordinates to frame bounds - h_frame, w_frame = frame.shape[:2] - x = max(0, min(x, w_frame - 1)) - y = max(0, min(y, h_frame - 1)) - w = min(w, w_frame - x) - h = min(h, h_frame - y) + # Apply EFFECTIVE crop regardless of whether a base crop exists, to enable follow and out-of-frame pad + x, y, w, h = self._get_effective_crop_rect_for_frame(frame_number or self.current_frame) - if w > 0 and h > 0: - frame = frame[y : y + h, x : x + w] - else: - return None + # Allow out-of-bounds by padding with black so center can remain when near edges + h_frame, w_frame = frame.shape[:2] + pad_left = max(0, -x) + pad_top = max(0, -y) + pad_right = max(0, (x + w) - w_frame) + pad_bottom = max(0, (y + h) - h_frame) + if any(p > 0 for p in (pad_left, pad_top, pad_right, pad_bottom)): + frame = cv2.copyMakeBorder( + frame, + pad_top, + pad_bottom, + pad_left, + pad_right, + borderType=cv2.BORDER_CONSTANT, + value=(0, 0, 0), + ) + x = x + pad_left + y = y + pad_top + w_frame, h_frame = frame.shape[1], frame.shape[0] + + # Clamp crop to padded frame + x = max(0, min(x, w_frame - 1)) + y = max(0, min(y, h_frame - 1)) + w = min(w, w_frame - x) + h = min(h, h_frame - y) + if w <= 0 or h <= 0: + return None + frame = frame[y : y + h, x : x + w] # Apply brightness and contrast frame = self.apply_brightness_contrast(frame) - # Apply rotation - if self.rotation_angle != 0: - frame = self.apply_rotation(frame) - # Apply zoom and resize directly to final output dimensions if self.zoom_factor != 1.0: height, width = frame.shape[:2]