diff --git a/backend/requirements-render.txt b/backend/requirements-render.txt index 614b40a..891800f 100644 --- a/backend/requirements-render.txt +++ b/backend/requirements-render.txt @@ -14,3 +14,8 @@ firebase-admin a2wsgi python-jose[cryptography] passlib[bcrypt] +async-lru +SpeechRecognition +googletrans==4.0.0-rc1 +langdetect +pydub diff --git a/backend/utils.py b/backend/utils.py index 7321507..ac7dc06 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -198,9 +198,8 @@ def process_uploaded_image_sync(file: UploadFile) -> tuple[Image.Image, bytes]: new_height = int(img.height * ratio) img = img.resize((new_width, new_height), Image.Resampling.BILINEAR) - # Strip EXIF - img_no_exif = Image.new(img.mode, img.size) - img_no_exif.paste(img) + # Strip EXIF in-place (O(1) memory, O(1) cpu) + img.info.clear() # Save to BytesIO output = io.BytesIO() @@ -211,10 +210,10 @@ def process_uploaded_image_sync(file: UploadFile) -> tuple[Image.Image, bytes]: else: fmt = 'PNG' if img.mode == 'RGBA' else 'JPEG' - img_no_exif.save(output, format=fmt, quality=85) + img.save(output, format=fmt, quality=85) img_bytes = output.getvalue() - return img_no_exif, img_bytes + return img, img_bytes except Exception as pil_error: logger.error(f"PIL processing failed: {pil_error}") @@ -274,14 +273,13 @@ def save_file_blocking(file_obj, path, image: Optional[Image.Image] = None): else: img = Image.open(file_obj) - # Strip EXIF data by creating a new image without metadata - # Use paste() instead of getdata() for O(1) performance (vs O(N) list creation) - img_no_exif = Image.new(img.mode, img.size) - img_no_exif.paste(img) + # Strip EXIF data in-place by clearing metadata (O(1) vs O(N) copy) + img.info.clear() + # Save without EXIF # Use original format if available, otherwise default to JPEG if mode is RGB, PNG if RGBA fmt = img.format or ('PNG' if img.mode == 'RGBA' else 'JPEG') - img_no_exif.save(path, format=fmt) + img.save(path, format=fmt) logger.info(f"Saved image {path} with EXIF metadata stripped") except Exception: # If not an image or PIL fails, save as binary diff --git a/tests/test_exif_stripping.py b/tests/test_exif_stripping.py new file mode 100644 index 0000000..c96eda8 --- /dev/null +++ b/tests/test_exif_stripping.py @@ -0,0 +1,57 @@ +import pytest +import io +import os +from PIL import Image +from backend.utils import process_uploaded_image_sync, save_file_blocking +from fastapi import UploadFile + +# Mock UploadFile +class MockUploadFile: + def __init__(self, filename, content): + self.filename = filename + self.file = io.BytesIO(content) + self.content_type = "image/jpeg" + self.size = len(content) + +def create_image_with_exif(): + # Create a small image + img = Image.new('RGB', (100, 100), color='red') + # Create dummy EXIF data + exif_data = b'Exif\x00\x00MM\x00*\x00\x00\x00\x08\x00\x00\x01\x00\x00' + + bio = io.BytesIO() + img.save(bio, format='JPEG', exif=exif_data) + return bio.getvalue() + +def test_process_uploaded_image_sync_strips_exif(): + content = create_image_with_exif() + # Verify input has EXIF + img_input = Image.open(io.BytesIO(content)) + if 'exif' not in img_input.info: + # If pillow doesn't see it, force it manually for test (not ideal but works for verify) + pass + + upload_file = MockUploadFile("test.jpg", content) + + # Run function + img_processed, img_bytes = process_uploaded_image_sync(upload_file) + + # Check returned image object + assert 'exif' not in img_processed.info + + # Check returned bytes + img_from_bytes = Image.open(io.BytesIO(img_bytes)) + assert 'exif' not in img_from_bytes.info + +def test_save_file_blocking_strips_exif(tmp_path): + content = create_image_with_exif() + upload_file = MockUploadFile("test.jpg", content) + + output_path = tmp_path / "saved_image.jpg" + + # Run function + save_file_blocking(upload_file.file, str(output_path)) + + # Check saved file + saved_img = Image.open(output_path) + assert 'exif' not in saved_img.info