mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
fix(metadata): preserve workflow when recipe images convert to webp
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import piexif
|
||||
import pytest
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
from py.services.recipes.analysis_service import RecipeAnalysisService
|
||||
from py.services.recipes.errors import (
|
||||
@@ -13,6 +16,7 @@ from py.services.recipes.errors import (
|
||||
RecipeValidationError,
|
||||
)
|
||||
from py.services.recipes.persistence_service import RecipePersistenceService
|
||||
from py.utils.exif_utils import ExifUtils
|
||||
|
||||
|
||||
class DummyExifUtils:
|
||||
@@ -420,6 +424,56 @@ async def test_save_recipe_derives_allowed_fields_from_raw_metadata(tmp_path):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_recipe_preserves_workflow_when_png_is_converted_to_webp(tmp_path):
|
||||
class DummyScanner:
|
||||
def __init__(self, root):
|
||||
self.recipes_dir = str(root)
|
||||
|
||||
async def find_recipes_by_fingerprint(self, fingerprint):
|
||||
return []
|
||||
|
||||
async def add_recipe(self, recipe_data):
|
||||
return None
|
||||
|
||||
png_info = PngImagePlugin.PngInfo()
|
||||
png_info.add_text("parameters", "prompt text\nSteps: 20")
|
||||
png_info.add_text("workflow", '{"nodes":[{"id":1}]}')
|
||||
|
||||
image_buffer = BytesIO()
|
||||
Image.new("RGB", (96, 48), color="purple").save(
|
||||
image_buffer, format="PNG", pnginfo=png_info
|
||||
)
|
||||
|
||||
service = RecipePersistenceService(
|
||||
exif_utils=ExifUtils,
|
||||
card_preview_width=64,
|
||||
logger=logging.getLogger("test"),
|
||||
)
|
||||
|
||||
result = await service.save_recipe(
|
||||
recipe_scanner=DummyScanner(tmp_path),
|
||||
image_bytes=image_buffer.getvalue(),
|
||||
image_base64=None,
|
||||
name="Workflow Recipe",
|
||||
tags=["workflow"],
|
||||
metadata={"base_model": "sd", "loras": []},
|
||||
extension=".png",
|
||||
)
|
||||
|
||||
image_path = Path(result.payload["image_path"])
|
||||
exif_dict = piexif.load(str(image_path))
|
||||
assert (
|
||||
exif_dict["0th"][piexif.ImageIFD.ImageDescription].decode("utf-8")
|
||||
== 'Workflow:{"nodes":[{"id":1}]}'
|
||||
)
|
||||
|
||||
user_comment = exif_dict["Exif"][piexif.ExifIFD.UserComment]
|
||||
decoded_comment = user_comment[8:].decode("utf-16be")
|
||||
assert "prompt text" in decoded_comment
|
||||
assert "Recipe metadata:" in decoded_comment
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_recipe_strips_checkpoint_local_fields(tmp_path):
|
||||
exif_utils = DummyExifUtils()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import json
|
||||
|
||||
import piexif
|
||||
from PIL import Image, PngImagePlugin
|
||||
|
||||
from py.utils.exif_utils import ExifUtils
|
||||
|
||||
|
||||
@@ -59,3 +62,82 @@ def test_append_recipe_metadata_includes_checkpoint(monkeypatch, tmp_path):
|
||||
"baseModel": "Illustrious",
|
||||
}
|
||||
assert payload["base_model"] == "Illustrious"
|
||||
|
||||
|
||||
def test_optimize_image_preserves_workflow_when_converting_png_to_webp(tmp_path):
|
||||
image_path = tmp_path / "source.png"
|
||||
png_info = PngImagePlugin.PngInfo()
|
||||
png_info.add_text("parameters", "prompt text\nSteps: 20")
|
||||
png_info.add_text("workflow", json.dumps({"nodes": [{"id": 1}]}))
|
||||
|
||||
Image.new("RGB", (64, 32), color="red").save(image_path, pnginfo=png_info)
|
||||
|
||||
optimized_data, extension = ExifUtils.optimize_image(
|
||||
str(image_path),
|
||||
target_width=32,
|
||||
format="webp",
|
||||
quality=85,
|
||||
preserve_metadata=True,
|
||||
)
|
||||
|
||||
optimized_path = tmp_path / f"optimized{extension}"
|
||||
optimized_path.write_bytes(optimized_data)
|
||||
|
||||
exif_dict = piexif.load(str(optimized_path))
|
||||
assert (
|
||||
exif_dict["0th"][piexif.ImageIFD.ImageDescription].decode("utf-8")
|
||||
== 'Workflow:{"nodes": [{"id": 1}]}'
|
||||
)
|
||||
user_comment = exif_dict["Exif"][piexif.ExifIFD.UserComment]
|
||||
assert user_comment.startswith(b"UNICODE\0")
|
||||
assert user_comment[8:].decode("utf-16be") == "prompt text\nSteps: 20"
|
||||
|
||||
|
||||
def test_update_image_metadata_preserves_webp_workflow(tmp_path):
|
||||
image_path = tmp_path / "recipe.webp"
|
||||
exif_dict = {
|
||||
"0th": {
|
||||
piexif.ImageIFD.ImageDescription: 'Workflow:{"nodes":[{"id":1}]}',
|
||||
},
|
||||
"Exif": {
|
||||
piexif.ExifIFD.UserComment: b"UNICODE\0"
|
||||
+ "prompt text".encode("utf-16be")
|
||||
},
|
||||
}
|
||||
Image.new("RGB", (32, 32), color="blue").save(
|
||||
image_path, format="WEBP", exif=piexif.dump(exif_dict), quality=85
|
||||
)
|
||||
|
||||
ExifUtils.update_image_metadata(
|
||||
str(image_path), 'prompt text\nRecipe metadata: {"title":"recipe"}'
|
||||
)
|
||||
|
||||
updated_exif = piexif.load(str(image_path))
|
||||
assert (
|
||||
updated_exif["0th"][piexif.ImageIFD.ImageDescription].decode("utf-8")
|
||||
== 'Workflow:{"nodes":[{"id":1}]}'
|
||||
)
|
||||
updated_comment = updated_exif["Exif"][piexif.ExifIFD.UserComment]
|
||||
assert (
|
||||
updated_comment[8:].decode("utf-16be")
|
||||
== 'prompt text\nRecipe metadata: {"title":"recipe"}'
|
||||
)
|
||||
|
||||
|
||||
def test_update_image_metadata_preserves_png_workflow(tmp_path):
|
||||
image_path = tmp_path / "recipe.png"
|
||||
png_info = PngImagePlugin.PngInfo()
|
||||
png_info.add_text("parameters", "prompt text")
|
||||
png_info.add_text("workflow", '{"nodes":[{"id":1}]}')
|
||||
Image.new("RGB", (32, 32), color="green").save(image_path, pnginfo=png_info)
|
||||
|
||||
ExifUtils.update_image_metadata(
|
||||
str(image_path), 'prompt text\nRecipe metadata: {"title":"recipe"}'
|
||||
)
|
||||
|
||||
with Image.open(image_path) as img:
|
||||
assert img.info["workflow"] == '{"nodes":[{"id":1}]}'
|
||||
assert (
|
||||
img.info["parameters"]
|
||||
== 'prompt text\nRecipe metadata: {"title":"recipe"}'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user