fix(phase1): fix deprecated PIL APIs, private API, exception handling, magic numbers, transparency, DoS prevention
This commit is contained in:
@@ -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''
|
||||
|
||||
Reference in New Issue
Block a user