diff --git a/metadata_utils.py b/metadata_utils.py index 7e0beec..e3d2516 100644 --- a/metadata_utils.py +++ b/metadata_utils.py @@ -3,7 +3,7 @@ Compatibility shim for MetaHub Save Node metadata helpers. The implementation is kept in metadata_utils_impl.py; this module exposes the same public API expected by the save nodes while pinning the published node -version to 1.1.2. +version to 1.1.3. """ try: @@ -11,7 +11,7 @@ try: except ImportError: import metadata_utils_impl as _impl -METAHUB_SAVE_NODE_VERSION = "1.1.2" +METAHUB_SAVE_NODE_VERSION = "1.1.3" _impl.METAHUB_SAVE_NODE_VERSION = METAHUB_SAVE_NODE_VERSION for _name in dir(_impl): diff --git a/metadata_utils_impl.py b/metadata_utils_impl.py index daa5fbe..0e4ada6 100644 --- a/metadata_utils_impl.py +++ b/metadata_utils_impl.py @@ -14,7 +14,7 @@ from xml.sax.saxutils import escape as xml_escape import numpy as np from PIL import Image, PngImagePlugin -METAHUB_SAVE_NODE_VERSION = "1.0.9" +METAHUB_SAVE_NODE_VERSION = "1.1.3" try: import piexif @@ -315,6 +315,28 @@ def collect_version_info() -> Dict[str, Optional[str]]: # METADATA FORMATTING # ============================================================================ +def make_civitai_safe_text(value: str) -> str: + """ + Normalizes common smart punctuation for legacy A1111/Civitai text readers. + """ + if value is None: + return "" + replacements = { + "\u2018": "'", + "\u2019": "'", + "\u201c": '"', + "\u201d": '"', + "\u2013": "-", + "\u2014": "-", + "\u2026": "...", + "\u00a0": " ", + } + text = str(value) + for source, replacement in replacements.items(): + text = text.replace(source, replacement) + return text + + def build_a1111_metadata(params: dict) -> str: """ Builds A1111/Civitai compatible metadata string for tEXt chunk. @@ -836,7 +858,7 @@ def save_png_with_metadata(image: Image.Image, image_path: str, a1111_metadata: """ try: png_info = PngImagePlugin.PngInfo() - png_info.add_text("parameters", a1111_metadata or "") + png_info.add_text("parameters", make_civitai_safe_text(a1111_metadata)) imh_json = json.dumps(imh_metadata or {}, ensure_ascii=False) png_info.add_itxt("imagemetahub_data", imh_json) workflow_text = _serialize_metadata_json_ascii(imh_metadata.get("workflow")) @@ -861,9 +883,10 @@ def _build_exif_bytes(a1111_metadata: str, imh_metadata: dict) -> Optional[bytes imh_json = json.dumps(imh_metadata or {}, ensure_ascii=False) if imh_json: exif_dict["0th"][piexif.ImageIFD.ImageDescription] = imh_json.encode("utf-8", errors="replace") - if a1111_metadata: + civitai_metadata = make_civitai_safe_text(a1111_metadata) + if civitai_metadata: exif_dict["Exif"][piexif.ExifIFD.UserComment] = piexif_helper.UserComment.dump( - a1111_metadata, encoding="unicode" + civitai_metadata, encoding="unicode" ) return piexif.dump(exif_dict) except Exception: @@ -889,7 +912,7 @@ def save_jpeg_with_metadata( if exif_bytes: save_kwargs["exif"] = exif_bytes elif a1111_metadata: - save_kwargs["comment"] = a1111_metadata.encode("utf-8", errors="replace") + save_kwargs["comment"] = make_civitai_safe_text(a1111_metadata).encode("utf-8", errors="replace") if xmp_bytes: save_kwargs["xmp"] = xmp_bytes try: @@ -922,7 +945,7 @@ def save_webp_with_metadata( if exif_bytes: save_kwargs["exif"] = exif_bytes elif a1111_metadata: - save_kwargs["comment"] = a1111_metadata + save_kwargs["comment"] = make_civitai_safe_text(a1111_metadata) if xmp_bytes: save_kwargs["xmp"] = xmp_bytes try: diff --git a/pyproject.toml b/pyproject.toml index 144eb6d..1c946c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "imagemetahub-comfyui-save" description = "Official companion node for Image MetaHub. Saves A1111/Civitai-compatible parameters plus extended Image MetaHub metadata (workflow JSON, SHA256 model hashes, VRAM peak/total, GPU device, generation time, and steps/sec) for reproducibility and benchmarking." -version = "1.1.2" +version = "1.1.3" license = {file = "LICENSE"} # classifiers = [ # # For OS-independent nodes (works on all operating systems) diff --git a/tests/test_metadata_utils.py b/tests/test_metadata_utils.py index ce78af2..757cd5d 100644 --- a/tests/test_metadata_utils.py +++ b/tests/test_metadata_utils.py @@ -16,6 +16,7 @@ from metadata_utils import ( get_next_filename, get_output_directory, get_workflow_json, + make_civitai_safe_text, resolve_filename_pattern_path, save_png_with_metadata, ) @@ -316,7 +317,7 @@ def test_extract_workflow_attribution_from_node_properties(): assert attribution["token"] == "imhcrt_br_creator_workflow_v1_random" assert attribution["source"] == "metahub_save_node" - assert attribution["node_version"] == "1.0.9" + assert attribution["node_version"] == "1.1.3" def test_build_imh_metadata_includes_attribution_without_a1111_parameters(): @@ -401,6 +402,42 @@ def test_build_a1111_metadata_formats_text(): assert "Model hash: abcdef1234" in text +def test_make_civitai_safe_text_normalizes_smart_punctuation(): + text = "McDonald\u2019s \u201cquote\u201d \u2013 dash \u2014 dash\u2026space\u00a0here" + + assert make_civitai_safe_text(text) == "McDonald's \"quote\" - dash - dash...space here" + + +def test_save_png_with_metadata_sanitizes_only_parameters_chunk(tmp_path): + imh_metadata = { + "prompt": "McDonald\u2019s \u201cquote\u201d", + "workflow": {"nodes": [{"title": "McDonald\u2019s \u201cquote\u201d"}]}, + "prompt_api": { + "1": { + "class_type": "CLIPTextEncode", + "inputs": {"text": "McDonald\u2019s \u201cquote\u201d"}, + } + }, + } + image = Image.new("RGB", (2, 2), color=(0, 0, 0)) + file_path = tmp_path / "civitai-safe.png" + + save_png_with_metadata( + image, + str(file_path), + "McDonald\u2019s \u201cquote\u201d \u2013 dash\u2026", + imh_metadata, + ) + + loaded = Image.open(file_path) + text = getattr(loaded, "text", {}) + + assert text["parameters"] == "McDonald's \"quote\" - dash..." + assert json.loads(text["imagemetahub_data"])["prompt"] == "McDonald\u2019s \u201cquote\u201d" + assert json.loads(text["workflow"])["nodes"][0]["title"] == "McDonald\u2019s \u201cquote\u201d" + assert json.loads(text["prompt"])["1"]["inputs"]["text"] == "McDonald\u2019s \u201cquote\u201d" + + def test_save_png_with_metadata_writes_workflow_prompt(tmp_path): imh_metadata = { "workflow": {"nodes": []},