diff --git a/backend/services/image_processing.py b/backend/services/image_processing.py index d871c0ef..05d8c688 100644 --- a/backend/services/image_processing.py +++ b/backend/services/image_processing.py @@ -88,6 +88,15 @@ class ImageProcessor: # Extract EXIF orientation (but don't apply yet) exif_orientation = self._extract_exif_orientation(image) + # Transform crop_bounds from EXIF-applied space to raw space + if crop_bounds and exif_orientation and exif_orientation > 1: + crop_bounds = self._transform_crop_bounds_by_exif( + crop_bounds, exif_orientation, original_size[0], original_size[1] + ) + msg = f"[CROP] EXIF {exif_orientation} transform applied: {crop_bounds}" + self.logger.info(msg) + print(f">>> {msg}") + # Smart cropping (on raw, un-rotated image so crop_bounds match AI analysis) cropped_image = image crop_size = None @@ -204,6 +213,44 @@ class ImageProcessor: self.logger.debug(f"Could not extract EXIF orientation: {e}") return None + def _transform_crop_bounds_by_exif( + self, crop_bounds: Dict, orientation: int, raw_width: int, raw_height: int + ) -> Dict: + """ + Transform crop_bounds from EXIF-applied space to raw image space. + + Gemini analyzes images with EXIF applied, returning crop_bounds in that space. + We need to transform them back to raw image coordinates before cropping. + + Args: + crop_bounds: {x, y, width, height} in EXIF-applied space + orientation: EXIF orientation value (1-8) + raw_width: raw image width (before EXIF) + raw_height: raw image height (before EXIF) + + Returns: + Transformed crop_bounds in raw image space + """ + if orientation == 1: + # No rotation, use as-is + return crop_bounds + elif orientation == 6: + # Rotate 90 CW: raw (w×h) appears as displayed (h×w) + # Transform from displayed (h×w) back to raw (w×h) + x, y, w, h = crop_bounds['x'], crop_bounds['y'], crop_bounds['width'], crop_bounds['height'] + # After 90° CW: (x,y) in raw → (raw_height - y, x) in displayed + # Reverse: (x,y) in displayed → (y, raw_width - x) in raw + return { + 'x': y, + 'y': raw_width - x - w, + 'width': h, + 'height': w + } + else: + # Other orientations: return as-is (TODO: implement if needed) + self.logger.warning(f"EXIF orientation {orientation} not supported for crop_bounds transform, using as-is") + return crop_bounds + def _rotate_by_orientation( self, image: Image.Image, orientation: int ) -> Image.Image: