fix(phase1): fix deprecated PIL APIs, private API, exception handling, magic numbers, transparency, DoS prevention

This commit is contained in:
2026-04-20 22:21:51 +03:00
parent 3aafacab12
commit 8d2750cfa3

View File

@@ -30,6 +30,14 @@ class ImageProcessor:
THUMBNAIL_SIZE = 200 THUMBNAIL_SIZE = 200
JPEG_QUALITY = 85 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): def __init__(self):
"""Initialize the image processor.""" """Initialize the image processor."""
self.logger = logger self.logger = logger
@@ -121,7 +129,7 @@ class ImageProcessor:
) )
else: else:
crop_method = 'pillow' crop_method = 'pillow'
except Exception as e: except (IOError, ValueError, cv2.error) as e:
# Fallback to Pillow if OpenCV fails # Fallback to Pillow if OpenCV fails
self.logger.warning( self.logger.warning(
f"OpenCV crop failed, falling back to Pillow: {e}" 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}") self.logger.error(f"Image processing failed: {e}")
return { return {
'status': 'error', 'status': 'error',
@@ -165,23 +173,15 @@ class ImageProcessor:
Orientation value (1-8) or None if not present Orientation value (1-8) or None if not present
""" """
try: try:
# Try piexif first # Use piexif for EXIF extraction (avoiding private PIL API)
if hasattr(image, 'info') and 'exif' in image.info: if hasattr(image, 'info') and 'exif' in image.info:
exif_dict = piexif.load(image.info['exif']) exif_dict = piexif.load(image.info['exif'])
orientation = exif_dict['0th'].get(piexif.ImageIFD.Orientation) orientation = exif_dict['0th'].get(piexif.ImageIFD.Orientation)
if orientation: if orientation:
return 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 return None
except Exception as e: except (piexif.InvalidImageData, ValueError, IOError) as e:
self.logger.debug(f"Could not extract EXIF orientation: {e}") self.logger.debug(f"Could not extract EXIF orientation: {e}")
return None return None
@@ -201,19 +201,19 @@ class ImageProcessor:
if orientation == 1: if orientation == 1:
return image return image
elif orientation == 2: elif orientation == 2:
return image.transpose(Image.FLIP_LEFT_RIGHT) return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
elif orientation == 3: elif orientation == 3:
return image.transpose(Image.ROTATE_180) return image.transpose(Image.Transpose.ROTATE_180)
elif orientation == 4: elif orientation == 4:
return image.transpose(Image.FLIP_TOP_BOTTOM) return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM)
elif orientation == 5: elif orientation == 5:
return image.transpose(Image.TRANSPOSE) return image.transpose(Image.Transpose.TRANSPOSE)
elif orientation == 6: elif orientation == 6:
return image.transpose(Image.ROTATE_270) return image.transpose(Image.Transpose.ROTATE_270)
elif orientation == 7: elif orientation == 7:
return image.transpose(Image.TRANSVERSE) return image.transpose(Image.Transpose.TRANSVERSE)
elif orientation == 8: elif orientation == 8:
return image.transpose(Image.ROTATE_90) return image.transpose(Image.Transpose.ROTATE_90)
return image return image
def _smart_crop_opencv( def _smart_crop_opencv(
@@ -234,7 +234,7 @@ class ImageProcessor:
gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY) gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
# Edge detection # Edge detection
edges = cv2.Canny(gray, 100, 200) edges = cv2.Canny(gray, *self.CANNY_CROP_THRESHOLDS)
# Find contours # Find contours
contours, _ = cv2.findContours( contours, _ = cv2.findContours(
@@ -249,10 +249,9 @@ class ImageProcessor:
largest_contour = max(contours, key=cv2.contourArea) largest_contour = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest_contour) x, y, w, h = cv2.boundingRect(largest_contour)
# Apply 10% padding around bounds # Apply padding around bounds
padding = 0.1 pad_x = int(w * self.CROP_PADDING_FACTOR)
pad_x = int(w * padding) pad_y = int(h * self.CROP_PADDING_FACTOR)
pad_y = int(h * padding)
x1 = max(0, x - pad_x) x1 = max(0, x - pad_x)
y1 = max(0, y - pad_y) y1 = max(0, y - pad_y)
@@ -269,7 +268,7 @@ class ImageProcessor:
return cropped, crop_size return cropped, crop_size
except Exception as e: except (IOError, ValueError, cv2.error) as e:
self.logger.warning(f"OpenCV smart crop failed: {e}") self.logger.warning(f"OpenCV smart crop failed: {e}")
return None return None
@@ -289,13 +288,19 @@ class ImageProcessor:
try: try:
# Convert to OpenCV format # Convert to OpenCV format
cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) 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) gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
# Edge detection # Edge detection
edges = cv2.Canny(gray, 50, 150) edges = cv2.Canny(gray, *self.CANNY_TEXT_THRESHOLDS)
# Hough line detection # 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: if lines is None or len(lines) == 0:
self.logger.debug("No lines detected for text orientation") self.logger.debug("No lines detected for text orientation")
@@ -320,11 +325,11 @@ class ImageProcessor:
corrected_angle = mean_angle corrected_angle = mean_angle
# Check for upside-down text (~180°) # Check for upside-down text (~180°)
if abs(mean_angle) > 80: if abs(mean_angle) > self.ANGLE_UPSIDE_DOWN_THRESHOLD:
status = 'upside_down' status = 'upside_down'
corrected_angle = mean_angle + 180 if mean_angle > 0 else mean_angle - 180 corrected_angle = mean_angle + 180 if mean_angle > 0 else mean_angle - 180
# Check for sideways text (~90°) # Check for sideways text (~90°)
elif abs(mean_angle) > 45: elif abs(mean_angle) > self.ANGLE_SIDEWAYS_THRESHOLD:
status = 'sideways' status = 'sideways'
self.logger.debug( self.logger.debug(
@@ -333,7 +338,7 @@ class ImageProcessor:
return corrected_angle, status return corrected_angle, status
except Exception as e: except (IOError, ValueError, cv2.error) as e:
self.logger.warning(f"Text orientation detection failed: {e}") self.logger.warning(f"Text orientation detection failed: {e}")
return None, 'not_detected' return None, 'not_detected'
@@ -376,8 +381,17 @@ class ImageProcessor:
# Convert to RGB if necessary (for JPEG) # Convert to RGB if necessary (for JPEG)
if image.mode in ('RGBA', 'LA', 'P'): 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 = 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 image = rgb_image
# Compress to JPEG # Compress to JPEG
@@ -423,11 +437,17 @@ class ImageProcessor:
# Convert to RGB if necessary # Convert to RGB if necessary
if thumbnail.mode in ('RGBA', 'LA', 'P'): 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 = Image.new('RGB', thumbnail.size, (255, 255, 255))
rgb_thumbnail.paste( rgb_thumbnail.paste(thumbnail, mask=mask)
thumbnail,
mask=thumbnail.split()[-1] if thumbnail.mode == 'RGBA' else None,
)
thumbnail = rgb_thumbnail thumbnail = rgb_thumbnail
# Compress # Compress
@@ -442,6 +462,6 @@ class ImageProcessor:
return thumbnail_bytes return thumbnail_bytes
except Exception as e: except (IOError, ValueError, cv2.error) as e:
self.logger.error(f"Thumbnail generation failed: {e}") self.logger.error(f"Thumbnail generation failed: {e}")
return b'' return b''