Enhance image handling and EXIF metadata processing in RecipeRoutes and ExifUtils

- Implemented image optimization in RecipeRoutes, resizing and converting uploaded images to WebP format while preserving metadata.
- Updated ExifUtils to support EXIF data handling for WebP images, ensuring compatibility with various image formats.
- Added a new method for optimizing images, allowing for better performance and quality in image uploads.
This commit is contained in:
Will Miao
2025-03-19 14:17:37 +08:00
parent 03dfe13769
commit c3aaef3916
4 changed files with 158 additions and 21 deletions

View File

@@ -409,12 +409,20 @@ class RecipeRoutes:
import uuid
recipe_id = str(uuid.uuid4())
# Save the image
image_ext = ".jpg"
image_filename = f"{recipe_id}{image_ext}"
# Optimize the image (resize and convert to WebP)
optimized_image, extension = ExifUtils.optimize_image(
image_data=image,
target_width=250,
format='webp',
quality=85,
preserve_metadata=True
)
# Save the optimized image
image_filename = f"{recipe_id}{extension}"
image_path = os.path.join(recipes_dir, image_filename)
with open(image_path, 'wb') as f:
f.write(image)
f.write(optimized_image)
# Create the recipe JSON
current_time = time.time()
@@ -638,7 +646,6 @@ class RecipeRoutes:
shutil.copy2(image_path, temp_path)
# Add recipe metadata to the image
from ..utils.exif_utils import ExifUtils
processed_path = ExifUtils.append_recipe_metadata(temp_path, recipe)
# Create a URL for the processed image

View File

@@ -3,6 +3,7 @@ import json
import logging
from typing import Dict, Optional, Any
from io import BytesIO
import os
from PIL import Image
import re
@@ -17,8 +18,8 @@ class ExifUtils:
try:
# First try to open as image to check format
with Image.open(image_path) as img:
if img.format not in ['JPEG', 'TIFF']:
# For non-JPEG/TIFF images, try to get EXIF through PIL
if img.format not in ['JPEG', 'TIFF', 'WEBP']:
# For non-JPEG/TIFF/WEBP images, try to get EXIF through PIL
exif = img._getexif()
if exif and piexif.ExifIFD.UserComment in exif:
user_comment = exif[piexif.ExifIFD.UserComment]
@@ -29,7 +30,7 @@ class ExifUtils:
return user_comment
return None
# For JPEG/TIFF, use piexif
# For JPEG/TIFF/WEBP, use piexif
exif_dict = piexif.load(image_path)
if piexif.ExifIFD.UserComment in exif_dict.get('Exif', {}):
@@ -52,7 +53,25 @@ class ExifUtils:
try:
# Load the image and its EXIF data
with Image.open(image_path) as img:
exif_dict = piexif.load(img.info.get('exif', b''))
# Get original format
img_format = img.format
# For WebP format, we need a different approach
if img_format == 'WEBP':
# WebP doesn't support standard EXIF through piexif
# We'll use PIL's exif parameter directly
exif_dict = {'Exif': {piexif.ExifIFD.UserComment: b'UNICODE\0' + user_comment.encode('utf-16be')}}
exif_bytes = piexif.dump(exif_dict)
# Save with the exif data
img.save(image_path, format='WEBP', exif=exif_bytes, quality=85)
return image_path
# For other formats, use the standard approach
try:
exif_dict = piexif.load(img.info.get('exif', b''))
except:
exif_dict = {'0th':{}, 'Exif':{}, 'GPS':{}, 'Interop':{}, '1st':{}}
# If no Exif dictionary exists, create one
if 'Exif' not in exif_dict:
@@ -143,4 +162,116 @@ class ExifUtils:
return user_comment[:recipe_marker_index].rstrip()
else:
# Metadata is in the middle of the string
return user_comment[:recipe_marker_index] + user_comment[next_line_index:]
return user_comment[:recipe_marker_index] + user_comment[next_line_index:]
@staticmethod
def optimize_image(image_data, target_width=250, format='webp', quality=85, preserve_metadata=True):
"""
Optimize an image by resizing and converting to WebP format
Args:
image_data: Binary image data or path to image file
target_width: Width to resize the image to (preserves aspect ratio)
format: Output format (default: webp)
quality: Output quality (0-100)
preserve_metadata: Whether to preserve EXIF metadata
Returns:
Tuple of (optimized_image_data, extension)
"""
try:
# Extract metadata if needed
user_comment = None
if preserve_metadata:
if isinstance(image_data, str) and os.path.exists(image_data):
# It's a file path
user_comment = ExifUtils.extract_user_comment(image_data)
img = Image.open(image_data)
else:
# It's binary data
temp_img = BytesIO(image_data)
img = Image.open(temp_img)
# Save to a temporary file to extract metadata
import tempfile
with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp_file:
temp_path = temp_file.name
temp_file.write(image_data)
user_comment = ExifUtils.extract_user_comment(temp_path)
os.unlink(temp_path)
else:
# Just open the image without extracting metadata
if isinstance(image_data, str) and os.path.exists(image_data):
img = Image.open(image_data)
else:
img = Image.open(BytesIO(image_data))
# Calculate new height to maintain aspect ratio
width, height = img.size
new_height = int(height * (target_width / width))
# Resize the image
resized_img = img.resize((target_width, new_height), Image.LANCZOS)
# Save to BytesIO in the specified format
output = BytesIO()
# WebP format
if format.lower() == 'webp':
resized_img.save(output, format='WEBP', quality=quality)
extension = '.webp'
# JPEG format
elif format.lower() in ('jpg', 'jpeg'):
resized_img.save(output, format='JPEG', quality=quality)
extension = '.jpg'
# PNG format
elif format.lower() == 'png':
resized_img.save(output, format='PNG', optimize=True)
extension = '.png'
else:
# Default to WebP
resized_img.save(output, format='WEBP', quality=quality)
extension = '.webp'
# Get the optimized image data
optimized_data = output.getvalue()
# If we need to preserve metadata, write it to a temporary file
if preserve_metadata and user_comment:
# For WebP format, we'll directly save with metadata
if format.lower() == 'webp':
# Create a new BytesIO with metadata
output_with_metadata = BytesIO()
# Create EXIF data with user comment
exif_dict = {'Exif': {piexif.ExifIFD.UserComment: b'UNICODE\0' + user_comment.encode('utf-16be')}}
exif_bytes = piexif.dump(exif_dict)
# Save with metadata
resized_img.save(output_with_metadata, format='WEBP', exif=exif_bytes, quality=quality)
optimized_data = output_with_metadata.getvalue()
else:
# For other formats, use the temporary file approach
import tempfile
with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as temp_file:
temp_path = temp_file.name
temp_file.write(optimized_data)
# Add the metadata back
ExifUtils.update_user_comment(temp_path, user_comment)
# Read the file with metadata
with open(temp_path, 'rb') as f:
optimized_data = f.read()
# Clean up
os.unlink(temp_path)
return optimized_data, extension
except Exception as e:
logger.error(f"Error optimizing image: {e}", exc_info=True)
# Return original data if optimization fails
if isinstance(image_data, str) and os.path.exists(image_data):
with open(image_data, 'rb') as f:
return f.read(), os.path.splitext(image_data)[1]
return image_data, '.jpg'

View File

@@ -74,8 +74,6 @@ class RecipeFormatParser(RecipeMetadataParser):
if not recipe_metadata:
return {"error": "No recipe metadata found", "loras": []}
logger.info("Found existing recipe metadata in image")
# Process the recipe metadata
loras = []
for lora in recipe_metadata.get('loras', []):
@@ -96,7 +94,7 @@ class RecipeFormatParser(RecipeMetadataParser):
exists_locally = lora_scanner.has_lora_hash(lora['hash'])
if exists_locally:
lora_cache = await lora_scanner.get_cached_data()
lora_item = next((item for item in lora_cache.raw_data if item['sha256'] == lora['hash']), None)
lora_item = next((item for item in lora_cache.raw_data if item['sha256'].lower() == lora['hash'].lower()), None)
if lora_item:
lora_entry['existsLocally'] = True
lora_entry['localPath'] = lora_item['file_path']

View File

@@ -4,14 +4,6 @@
transition: none !important; /* Disable any transitions that might affect display */
}
/* Ensure error messages maintain proper height and visibility */
.import-section .error-message {
min-height: 1.2em;
visibility: visible !important;
opacity: 1 !important;
display: block !important;
}
/* Import Mode Toggle */
.import-mode-toggle {
display: flex;
@@ -681,3 +673,12 @@
margin-right: 4px;
font-size: 0.9em;
}
/* Error message styling */
.error-message {
color: var(--lora-error);
font-size: 0.9em;
margin-top: 8px;
min-height: 20px; /* Ensure there's always space for the error message */
font-weight: 500;
}