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
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''