From 755e1a5bca0c6647d62b46e38c915459d9550f9d Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 23 Sep 2026 20:30:51 +0800 Subject: [PATCH] fix: fall back to source image dimensions for missing width/height When metadata extraction succeeds but no recognized latent source provides dimensions (e.g. img2img via VAEEncode), width/height now fall back to the source image size from the loaded pixels instead of the synthetic 1024x1024 starter preset. The starter preset for metadata-free images keeps its fixed size, and explicit overrides still win. --- docs/load-image-metadata.md | 5 +++- py/nodes/load_image_metadata.py | 15 +++++++++- tests/nodes/test_load_image_metadata.py | 38 +++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/load-image-metadata.md b/docs/load-image-metadata.md index ab3a3271..a90c4e35 100644 --- a/docs/load-image-metadata.md +++ b/docs/load-image-metadata.md @@ -75,7 +75,8 @@ prompt; UI-workflow-only subgraph definitions are not expanded or executed. Extraction errors do not stop this node. If an API prompt uses unsupported samplers, the node first tries the image's saved generation parameters. Any -remaining unavailable or invalid extracted fields use the SDXL starter defaults; +remaining unavailable or invalid extracted fields use the SDXL starter defaults +(width/height fall back to the source image dimensions instead); valid extracted fields are preserved. `readable_report` starts with **❌ ERROR** and explains each recovery or substitution. This also applies to existing nodes saved with `missing_settings=strict`; that legacy option no longer blocks @@ -147,6 +148,8 @@ when uniquely indexed; otherwise choose an SDXL checkpoint manually or supply original workflow for those cases. - Width/height come from a recognized latent source or fall back to source-image dimensions; resized/upscaled images can therefore need dimension overrides. + Only the synthetic starter preset for metadata-free images uses a fixed + 1024×1024 regardless of the source image size. - VAE, text encoder choice, CLIP skip, ControlNet and architecture-specific conditioning still need the appropriate nodes. No embedded code is executed and no external metadata service is contacted. diff --git a/py/nodes/load_image_metadata.py b/py/nodes/load_image_metadata.py index a9c11346..ddd30153 100644 --- a/py/nodes/load_image_metadata.py +++ b/py/nodes/load_image_metadata.py @@ -253,8 +253,21 @@ class LoadImageMetadataLM: if "model" in extracted.issues and "model_name" not in overrides: values.pop("checkpoint_name", None) values.pop("unet_name", None) + # Extraction without a recognized latent source (e.g. img2img) leaves + # width/height unset; the source image dimensions are the best + # estimate then. The synthetic starter preset keeps its fixed size. + image_fallback = not no_metadata and "source" not in extracted.issues + try: + image_height, image_width = int(pixels.shape[1]), int(pixels.shape[2]) + except (AttributeError, IndexError, TypeError, ValueError): + image_fallback = False for key, default in EMPTY_IMAGE_DEFAULTS.items(): - if key not in values: + if key in values: + continue + if image_fallback and key in ("width", "height"): + values[key] = image_width if key == "width" else image_height + notes.append(f"WARNING Missing {key}; using source image dimension {values[key]}.") + else: values[key] = default notes.append(f"ERROR: Missing {key}; using default {default!r}.") # Validate independently so one invalid value cannot erase the other diff --git a/tests/nodes/test_load_image_metadata.py b/tests/nodes/test_load_image_metadata.py index 17de96c9..6c53b2f9 100644 --- a/tests/nodes/test_load_image_metadata.py +++ b/tests/nodes/test_load_image_metadata.py @@ -96,6 +96,44 @@ def test_no_metadata_can_be_inspected_with_defaults(runtime): assert "No model resolved" in result[15] +def test_graph_without_recognized_latent_falls_back_to_image_size(runtime): + info = PngImagePlugin.PngInfo() + graph = { + "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "base.safetensors"}}, + "2": {"class_type": "CLIPTextEncode", "inputs": {"text": "pos", "clip": ["1", 1]}}, + "3": {"class_type": "CLIPTextEncode", "inputs": {"text": "neg", "clip": ["1", 1]}}, + "5": {"class_type": "KSampler", "inputs": { + "model": ["1", 0], "positive": ["2", 0], "negative": ["3", 0], + "latent_image": ["9", 0], "seed": 1, "steps": 20, "cfg": 7, + "sampler_name": "euler", "scheduler": "normal", "denoise": 1, + }}, + "9": {"class_type": "VAEEncode", "inputs": {"pixels": ["10", 0], "vae": ["1", 2]}}, + } + info.add_text("prompt", json.dumps(graph)) + Image.new("RGB", (16, 24)).save(runtime[0], pnginfo=info) + # The mocked loader returns pixels with shape (1, 24, 16, 3): H=24, W=16. + result = LoadImageMetadataLM().load_metadata("input.png") + assert result[12:14] == (16, 24) + assert "using source image dimension" in result[15] + assert "❌ ERROR" not in result[16] + + +def test_parameters_without_size_fall_back_to_image_size(runtime): + info = PngImagePlugin.PngInfo() + info.add_text("parameters", PARAMETERS.replace(", Size: 768x1024", "")) + Image.new("RGB", (16, 24)).save(runtime[0], pnginfo=info) + result = LoadImageMetadataLM().load_metadata("input.png") + assert result[12:14] == (16, 24) + + +def test_size_override_wins_over_image_size_fallback(runtime): + info = PngImagePlugin.PngInfo() + info.add_text("parameters", PARAMETERS.replace(", Size: 768x1024", "")) + Image.new("RGB", (16, 24)).save(runtime[0], pnginfo=info) + result = LoadImageMetadataLM().load_metadata("input.png", overrides_json='{"width": 512, "height": 640}') + assert result[12:14] == (512, 640) + + @pytest.mark.parametrize("override", [{"seed": -1}, {"steps": 2.5}, {"cfg": float("nan")}, {"sampler_name": "made_up"}, {"positive": ["1", 0]}, {"unknown": 1}]) def test_invalid_override_rejected(runtime, override): with pytest.raises((MetadataError, ValueError)):