From 8d2750cfa3681eabb4688c1af27d644b7a122f15 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Mon, 20 Apr 2026 22:21:51 +0300 Subject: [PATCH] fix(phase1): fix deprecated PIL APIs, private API, exception handling, magic numbers, transparency, DoS prevention --- backend/services/image_processing.py | 92 +++++++++++++++++----------- 1 file changed, 56 insertions(+), 36 deletions(-) diff --git a/backend/services/image_processing.py b/backend/services/image_processing.py index 2273f619..54d95cf3 100644 --- a/backend/services/image_processing.py +++ b/backend/services/image_processing.py @@ -30,6 +30,14 @@ class ImageProcessor: THUMBNAIL_SIZE = 200 JPEG_QUALITY = 85 + # Algorithm parameters (Issue 4: DRY Violation - Magic Numbers) + CANNY_CROP_THRESHOLDS = (100, 200) + CANNY_TEXT_THRESHOLDS = (50, 150) + HOUGH_THRESHOLD = 100 + CROP_PADDING_FACTOR = 0.1 + ANGLE_UPSIDE_DOWN_THRESHOLD = 80 # degrees + ANGLE_SIDEWAYS_THRESHOLD = 45 # degrees + def __init__(self): """Initialize the image processor.""" self.logger = logger @@ -121,7 +129,7 @@ class ImageProcessor: ) else: crop_method = 'pillow' - except Exception as e: + except (IOError, ValueError, cv2.error) as e: # Fallback to Pillow if OpenCV fails self.logger.warning( f"OpenCV crop failed, falling back to Pillow: {e}" @@ -148,7 +156,7 @@ class ImageProcessor: }, } - except Exception as e: + except (IOError, ValueError, cv2.error) as e: self.logger.error(f"Image processing failed: {e}") return { 'status': 'error', @@ -165,23 +173,15 @@ class ImageProcessor: Orientation value (1-8) or None if not present """ try: - # Try piexif first + # Use piexif for EXIF extraction (avoiding private PIL API) if hasattr(image, 'info') and 'exif' in image.info: exif_dict = piexif.load(image.info['exif']) orientation = exif_dict['0th'].get(piexif.ImageIFD.Orientation) if orientation: return orientation - # Fallback to PIL EXIF - exif_data = image._getexif() - if exif_data: - for tag_id, value in exif_data.items(): - tag = TAGS.get(tag_id, tag_id) - if tag == 'Orientation': - return value - return None - except Exception as e: + except (piexif.InvalidImageData, ValueError, IOError) as e: self.logger.debug(f"Could not extract EXIF orientation: {e}") return None @@ -201,19 +201,19 @@ class ImageProcessor: if orientation == 1: return image elif orientation == 2: - return image.transpose(Image.FLIP_LEFT_RIGHT) + return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) elif orientation == 3: - return image.transpose(Image.ROTATE_180) + return image.transpose(Image.Transpose.ROTATE_180) elif orientation == 4: - return image.transpose(Image.FLIP_TOP_BOTTOM) + return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) elif orientation == 5: - return image.transpose(Image.TRANSPOSE) + return image.transpose(Image.Transpose.TRANSPOSE) elif orientation == 6: - return image.transpose(Image.ROTATE_270) + return image.transpose(Image.Transpose.ROTATE_270) elif orientation == 7: - return image.transpose(Image.TRANSVERSE) + return image.transpose(Image.Transpose.TRANSVERSE) elif orientation == 8: - return image.transpose(Image.ROTATE_90) + return image.transpose(Image.Transpose.ROTATE_90) return image def _smart_crop_opencv( @@ -234,7 +234,7 @@ class ImageProcessor: gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY) # Edge detection - edges = cv2.Canny(gray, 100, 200) + edges = cv2.Canny(gray, *self.CANNY_CROP_THRESHOLDS) # Find contours contours, _ = cv2.findContours( @@ -249,10 +249,9 @@ class ImageProcessor: largest_contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(largest_contour) - # Apply 10% padding around bounds - padding = 0.1 - pad_x = int(w * padding) - pad_y = int(h * padding) + # Apply padding around bounds + pad_x = int(w * self.CROP_PADDING_FACTOR) + pad_y = int(h * self.CROP_PADDING_FACTOR) x1 = max(0, x - pad_x) y1 = max(0, y - pad_y) @@ -269,7 +268,7 @@ class ImageProcessor: return cropped, crop_size - except Exception as e: + except (IOError, ValueError, cv2.error) as e: self.logger.warning(f"OpenCV smart crop failed: {e}") return None @@ -289,13 +288,19 @@ class ImageProcessor: try: # Convert to OpenCV format cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + # DoS prevention: check resolution + if cv_image.shape[0] * cv_image.shape[1] > 2000 * 2000: # >4MP + self.logger.warning("ROI too large for text detection, skipping") + return None, 'not_detected' + gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY) # Edge detection - edges = cv2.Canny(gray, 50, 150) + edges = cv2.Canny(gray, *self.CANNY_TEXT_THRESHOLDS) # Hough line detection - lines = cv2.HoughLines(edges, 1, np.pi / 180, 100) + lines = cv2.HoughLines(edges, 1, np.pi / 180, self.HOUGH_THRESHOLD) if lines is None or len(lines) == 0: self.logger.debug("No lines detected for text orientation") @@ -320,11 +325,11 @@ class ImageProcessor: corrected_angle = mean_angle # Check for upside-down text (~180°) - if abs(mean_angle) > 80: + if abs(mean_angle) > self.ANGLE_UPSIDE_DOWN_THRESHOLD: status = 'upside_down' corrected_angle = mean_angle + 180 if mean_angle > 0 else mean_angle - 180 # Check for sideways text (~90°) - elif abs(mean_angle) > 45: + elif abs(mean_angle) > self.ANGLE_SIDEWAYS_THRESHOLD: status = 'sideways' self.logger.debug( @@ -333,7 +338,7 @@ class ImageProcessor: return corrected_angle, status - except Exception as e: + except (IOError, ValueError, cv2.error) as e: self.logger.warning(f"Text orientation detection failed: {e}") return None, 'not_detected' @@ -376,8 +381,17 @@ class ImageProcessor: # Convert to RGB if necessary (for JPEG) if image.mode in ('RGBA', 'LA', 'P'): + # Extract alpha channel if present + mask = None + if image.mode == 'RGBA': + alpha = image.split()[3] + mask = alpha + elif image.mode == 'LA': + alpha = image.split()[1] + mask = alpha + rgb_image = Image.new('RGB', image.size, (255, 255, 255)) - rgb_image.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None) + rgb_image.paste(image, mask=mask) image = rgb_image # Compress to JPEG @@ -423,11 +437,17 @@ class ImageProcessor: # Convert to RGB if necessary if thumbnail.mode in ('RGBA', 'LA', 'P'): + # Extract alpha channel if present + mask = None + if thumbnail.mode == 'RGBA': + alpha = thumbnail.split()[3] + mask = alpha + elif thumbnail.mode == 'LA': + alpha = thumbnail.split()[1] + mask = alpha + rgb_thumbnail = Image.new('RGB', thumbnail.size, (255, 255, 255)) - rgb_thumbnail.paste( - thumbnail, - mask=thumbnail.split()[-1] if thumbnail.mode == 'RGBA' else None, - ) + rgb_thumbnail.paste(thumbnail, mask=mask) thumbnail = rgb_thumbnail # Compress @@ -442,6 +462,6 @@ class ImageProcessor: return thumbnail_bytes - except Exception as e: + except (IOError, ValueError, cv2.error) as e: self.logger.error(f"Thumbnail generation failed: {e}") return b''