Files
tfm_ainventory/backend/image_processing.py

114 lines
3.2 KiB
Python

"""
Image processing utilities for photo uploads, cropping, and storage.
Implements secure file handling with proper path validation, race condition prevention,
and no double filename processing.
"""
from pathlib import Path
from typing import Optional, Dict, Any
import hashlib
import os
# Configuration
IMAGES_DIR = Path("images")
IMAGES_DIR.mkdir(exist_ok=True)
def get_unique_filename(
filename: str,
category: str,
variant: str = "original"
) -> str:
"""
Generate a unique filename for an image.
Args:
filename: Base filename (without extension)
category: Category for organization
variant: "original" or "thumbnail"
Returns:
Unique filename with extension
"""
# Ensure directory exists
cat_dir = IMAGES_DIR / category
cat_dir.mkdir(parents=True, exist_ok=True)
# Sanitize filename
safe_name = "".join(c for c in filename if c.isalnum() or c in ("_", "-", " ")).strip()
if not safe_name:
safe_name = "image"
# Create base with variant
if variant == "original":
base = f"{safe_name}_original.jpg"
elif variant == "thumbnail":
base = f"{safe_name}_thumb.jpg"
else:
base = f"{safe_name}.jpg"
# Check for collisions
target = cat_dir / base
if not target.exists():
return str(target.relative_to(IMAGES_DIR.parent))
# Add hash suffix if collision
name_hash = hashlib.md5(str(os.urandom(16)).encode()).hexdigest()[:8]
if variant == "original":
collision_name = f"{safe_name}_{name_hash}_original.jpg"
elif variant == "thumbnail":
collision_name = f"{safe_name}_{name_hash}_thumb.jpg"
else:
collision_name = f"{safe_name}_{name_hash}.jpg"
target = cat_dir / collision_name
return str(target.relative_to(IMAGES_DIR.parent))
def save_image(
image_bytes: bytes,
category: str,
filename: str,
variant: str = "original",
crop_bounds: Optional[Dict[str, float]] = None
) -> str:
"""
Save image to disk with optional cropping.
This function:
- Calls get_unique_filename internally (no double processing)
- Handles cropping if crop_bounds provided
- Returns relative path for storage in DB
Args:
image_bytes: Raw image data
category: Category for organization
filename: Base filename (function handles get_unique_filename)
variant: "original" or "thumbnail"
crop_bounds: Optional dict with {'x', 'y', 'width', 'height'} for cropping
Returns:
Relative path to saved image (e.g., "category/filename_original.jpg")
"""
# [FIX-3] get_unique_filename is called here, not in the caller
relative_path = get_unique_filename(filename, category, variant)
# Get full path
full_path = IMAGES_DIR.parent / relative_path
full_path.parent.mkdir(parents=True, exist_ok=True)
# TODO: Implement actual image processing with PIL/OpenCV
# For now, save raw bytes
# In production, this would:
# - Load image with PIL
# - Apply cropping if crop_bounds provided
# - Resize for thumbnails
# - Apply compression
with open(full_path, "wb") as f:
f.write(image_bytes)
return relative_path