From c46c8414b80ec0b2419a6d2ac6dec11cd477f524 Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Wed, 22 Apr 2026 11:13:09 +0300 Subject: [PATCH] feat: store original EXIF-stripped image for debugging - Save original image before crop/rotation with '_debug_original' variant - Store original_photo_path in labels_data.image_processing for debug access - Update DebugRotationPanel to display original image instead of processed - Update ItemDetailModal to pass original image path to debug panel - Enables accurate crop/rotation visualization with true original image --- backend/routers/items.py | 37 ++++++++++++++++++++++ frontend/components/DebugRotationPanel.tsx | 9 ++++-- frontend/components/ItemDetailModal.tsx | 13 ++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/backend/routers/items.py b/backend/routers/items.py index ff38c3a7..0b97fba5 100644 --- a/backend/routers/items.py +++ b/backend/routers/items.py @@ -594,6 +594,23 @@ def _auto_save_photo_from_extraction( # All validation passed, proceed with processing try: + # Save original image (EXIF-stripped, unprocessed) for debugging + category = db_item.category or "items" + existing_files = [] + cat_dir = Path("images") / category.lower() + if cat_dir.exists(): + existing_files = [f.name for f in cat_dir.iterdir() if f.is_file()] + + filename_base = db_item.name or f"item_{item_id}" + debug_filename = get_unique_filename(filename_base, category, existing_files, variant="debug_original") + + try: + original_debug_path = save_image(image_bytes, category, debug_filename.replace("_debug_original.jpg", ""), variant="debug_original") + log.info(f"[AUTO_SAVE] Saved original image for debugging: {original_debug_path}") + except Exception as e: + log.warning(f"[AUTO_SAVE] Failed to save debug original image: {e}") + original_debug_path = None + # Process image (crop + rotation + compression + thumbnail) processor = ImageProcessor() log.info(f"[AUTO_SAVE] Processing photo: crop_bounds={crop_bounds_validated}, rotation_degrees={rotation_degrees or 0}") @@ -664,6 +681,26 @@ def _auto_save_photo_from_extraction( db_item.photo_thumbnail_path = thumbnail_path db_item.photo_upload_date = datetime.now(timezone.utc) + # Store image processing metadata in labels_data (including original debug image path) + if not db_item.labels_data: + db_item.labels_data = "{}" + + try: + labels = json.loads(db_item.labels_data) + except (json.JSONDecodeError, TypeError): + labels = {} + + if "image_processing" not in labels: + labels["image_processing"] = {} + + # Add the original image path for debugging + labels["image_processing"]["original_photo_path"] = original_debug_path + labels["image_processing"]["crop_bounds"] = crop_bounds_validated + labels["image_processing"]["rotation_degrees"] = rotation_degrees or 0 + + db_item.labels_data = json.dumps(labels) + log.info(f"[AUTO_SAVE] Updated labels_data with original_photo_path: {original_debug_path}") + db.commit() db.refresh(db_item) diff --git a/frontend/components/DebugRotationPanel.tsx b/frontend/components/DebugRotationPanel.tsx index 03504af7..0875251d 100644 --- a/frontend/components/DebugRotationPanel.tsx +++ b/frontend/components/DebugRotationPanel.tsx @@ -7,6 +7,7 @@ import { X } from 'lucide-react'; interface DebugRotationPanelProps { item: Item; imageUrl: string; + originalPhotoPath?: string; originalCropBounds?: { x: number; y: number; width: number; height: number }; originalRotation?: number; imageProcessing?: any; @@ -16,6 +17,7 @@ interface DebugRotationPanelProps { export function DebugRotationPanel({ item, imageUrl, + originalPhotoPath, originalCropBounds, originalRotation = 0, imageProcessing, @@ -28,6 +30,9 @@ export function DebugRotationPanel({ const [imageLoaded, setImageLoaded] = useState(false); const [scaledDimensions, setScaledDimensions] = useState({ width: 0, height: 0, scale: 1 }); + // Use original photo path if available, otherwise fall back to processed image URL + const displayImageUrl = originalPhotoPath ? originalPhotoPath : imageUrl; + const commonAngles = [ { label: '0°', value: 0 }, { label: '+22°', value: 22 }, @@ -61,8 +66,8 @@ export function DebugRotationPanel({ addLog(`📐 Scaled to fit: ${scaledW.toFixed(0)}×${scaledH.toFixed(0)}px (scale ${scale.toFixed(2)}x)`); }; img.onerror = () => addLog('❌ Failed to load image'); - img.src = imageUrl; - }, [imageUrl]); + img.src = displayImageUrl; + }, [displayImageUrl]); useEffect(() => { if (!imageLoaded || !imageRef.current || !canvasRef.current || !originalCropBounds) return; diff --git a/frontend/components/ItemDetailModal.tsx b/frontend/components/ItemDetailModal.tsx index 22a32c2c..7b456f49 100644 --- a/frontend/components/ItemDetailModal.tsx +++ b/frontend/components/ItemDetailModal.tsx @@ -198,6 +198,19 @@ export default function ItemDetailModal({ { + try { + const parsed = JSON.parse(item.labels_data); + const origPath = parsed.image_processing?.original_photo_path; + return origPath ? buildPhotoUrl(backendUrl, origPath) : undefined; + } catch { + return undefined; + } + })() + : undefined + } originalCropBounds={item.labels_data ? JSON.parse(item.labels_data).image_processing?.crop_bounds : undefined} originalRotation={item.labels_data ? JSON.parse(item.labels_data).image_processing?.rotation_degrees : undefined} imageProcessing={item.labels_data ? JSON.parse(item.labels_data).image_processing : undefined}