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
|
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''
|
||||||
|
|||||||
Reference in New Issue
Block a user