mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
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:
@@ -409,12 +409,20 @@ class RecipeRoutes:
|
|||||||
import uuid
|
import uuid
|
||||||
recipe_id = str(uuid.uuid4())
|
recipe_id = str(uuid.uuid4())
|
||||||
|
|
||||||
# Save the image
|
# Optimize the image (resize and convert to WebP)
|
||||||
image_ext = ".jpg"
|
optimized_image, extension = ExifUtils.optimize_image(
|
||||||
image_filename = f"{recipe_id}{image_ext}"
|
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)
|
image_path = os.path.join(recipes_dir, image_filename)
|
||||||
with open(image_path, 'wb') as f:
|
with open(image_path, 'wb') as f:
|
||||||
f.write(image)
|
f.write(optimized_image)
|
||||||
|
|
||||||
# Create the recipe JSON
|
# Create the recipe JSON
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
@@ -638,7 +646,6 @@ class RecipeRoutes:
|
|||||||
shutil.copy2(image_path, temp_path)
|
shutil.copy2(image_path, temp_path)
|
||||||
|
|
||||||
# Add recipe metadata to the image
|
# Add recipe metadata to the image
|
||||||
from ..utils.exif_utils import ExifUtils
|
|
||||||
processed_path = ExifUtils.append_recipe_metadata(temp_path, recipe)
|
processed_path = ExifUtils.append_recipe_metadata(temp_path, recipe)
|
||||||
|
|
||||||
# Create a URL for the processed image
|
# Create a URL for the processed image
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, Optional, Any
|
from typing import Dict, Optional, Any
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
import os
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -17,8 +18,8 @@ class ExifUtils:
|
|||||||
try:
|
try:
|
||||||
# First try to open as image to check format
|
# First try to open as image to check format
|
||||||
with Image.open(image_path) as img:
|
with Image.open(image_path) as img:
|
||||||
if img.format not in ['JPEG', 'TIFF']:
|
if img.format not in ['JPEG', 'TIFF', 'WEBP']:
|
||||||
# For non-JPEG/TIFF images, try to get EXIF through PIL
|
# For non-JPEG/TIFF/WEBP images, try to get EXIF through PIL
|
||||||
exif = img._getexif()
|
exif = img._getexif()
|
||||||
if exif and piexif.ExifIFD.UserComment in exif:
|
if exif and piexif.ExifIFD.UserComment in exif:
|
||||||
user_comment = exif[piexif.ExifIFD.UserComment]
|
user_comment = exif[piexif.ExifIFD.UserComment]
|
||||||
@@ -29,7 +30,7 @@ class ExifUtils:
|
|||||||
return user_comment
|
return user_comment
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# For JPEG/TIFF, use piexif
|
# For JPEG/TIFF/WEBP, use piexif
|
||||||
exif_dict = piexif.load(image_path)
|
exif_dict = piexif.load(image_path)
|
||||||
|
|
||||||
if piexif.ExifIFD.UserComment in exif_dict.get('Exif', {}):
|
if piexif.ExifIFD.UserComment in exif_dict.get('Exif', {}):
|
||||||
@@ -52,7 +53,25 @@ class ExifUtils:
|
|||||||
try:
|
try:
|
||||||
# Load the image and its EXIF data
|
# Load the image and its EXIF data
|
||||||
with Image.open(image_path) as img:
|
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 no Exif dictionary exists, create one
|
||||||
if 'Exif' not in exif_dict:
|
if 'Exif' not in exif_dict:
|
||||||
@@ -143,4 +162,116 @@ class ExifUtils:
|
|||||||
return user_comment[:recipe_marker_index].rstrip()
|
return user_comment[:recipe_marker_index].rstrip()
|
||||||
else:
|
else:
|
||||||
# Metadata is in the middle of the string
|
# 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'
|
||||||
@@ -74,8 +74,6 @@ class RecipeFormatParser(RecipeMetadataParser):
|
|||||||
if not recipe_metadata:
|
if not recipe_metadata:
|
||||||
return {"error": "No recipe metadata found", "loras": []}
|
return {"error": "No recipe metadata found", "loras": []}
|
||||||
|
|
||||||
logger.info("Found existing recipe metadata in image")
|
|
||||||
|
|
||||||
# Process the recipe metadata
|
# Process the recipe metadata
|
||||||
loras = []
|
loras = []
|
||||||
for lora in recipe_metadata.get('loras', []):
|
for lora in recipe_metadata.get('loras', []):
|
||||||
@@ -96,7 +94,7 @@ class RecipeFormatParser(RecipeMetadataParser):
|
|||||||
exists_locally = lora_scanner.has_lora_hash(lora['hash'])
|
exists_locally = lora_scanner.has_lora_hash(lora['hash'])
|
||||||
if exists_locally:
|
if exists_locally:
|
||||||
lora_cache = await lora_scanner.get_cached_data()
|
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:
|
if lora_item:
|
||||||
lora_entry['existsLocally'] = True
|
lora_entry['existsLocally'] = True
|
||||||
lora_entry['localPath'] = lora_item['file_path']
|
lora_entry['localPath'] = lora_item['file_path']
|
||||||
|
|||||||
@@ -4,14 +4,6 @@
|
|||||||
transition: none !important; /* Disable any transitions that might affect display */
|
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 */
|
||||||
.import-mode-toggle {
|
.import-mode-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -681,3 +673,12 @@
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
font-size: 0.9em;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user