mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-06 22:10:14 -03:00
feat(SaveImageLM): expose webp_method and jpeg_subsampling as conditional node inputs
Add two new optional parameters to the Save Image node: - webp_method (INT, 0-6, default 6): Controls WebP compression level. 0=fastest/largest, 6=slowest/smallest. Previously hardcoded to 0. - jpeg_subsampling (INT, 0-2, default 0): Controls JPEG chroma subsampling. 0=4:4:4 (best quality), 1=4:2:2, 2=4:2:0. Frontend JS extension hides/disables each parameter when the selected file_format doesn't apply (e.g., webp_method is hidden when saving as PNG or JPEG). 7 new tests cover parameter plumbing and default consistency across INPUT_TYPES, save_images(), and process_image().
This commit is contained in:
@@ -220,6 +220,24 @@ class SaveImageLM:
|
|||||||
"tooltip": "Compression quality for JPEG and lossy WebP formats (1-100). Higher values mean better quality but larger files.",
|
"tooltip": "Compression quality for JPEG and lossy WebP formats (1-100). Higher values mean better quality but larger files.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
"webp_method": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 6,
|
||||||
|
"min": 0,
|
||||||
|
"max": 6,
|
||||||
|
"tooltip": "WebP compression method (0-6). 0=fastest/largest, 6=slowest/smallest. Only applies when file_format is 'webp'.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"jpeg_subsampling": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 2,
|
||||||
|
"tooltip": "JPEG chroma subsampling level. 0=4:4:4 (best quality), 1=4:2:2, 2=4:2:0 (smallest files). Only applies when file_format is 'jpeg'.",
|
||||||
|
},
|
||||||
|
),
|
||||||
"embed_workflow": (
|
"embed_workflow": (
|
||||||
"BOOLEAN",
|
"BOOLEAN",
|
||||||
{
|
{
|
||||||
@@ -756,6 +774,8 @@ class SaveImageLM:
|
|||||||
extra_pnginfo=None,
|
extra_pnginfo=None,
|
||||||
lossless_webp=True,
|
lossless_webp=True,
|
||||||
quality=100,
|
quality=100,
|
||||||
|
webp_method=6,
|
||||||
|
jpeg_subsampling=0,
|
||||||
embed_workflow=False,
|
embed_workflow=False,
|
||||||
save_with_metadata=True,
|
save_with_metadata=True,
|
||||||
add_counter_to_filename=True,
|
add_counter_to_filename=True,
|
||||||
@@ -810,15 +830,14 @@ class SaveImageLM:
|
|||||||
elif file_format == "jpeg":
|
elif file_format == "jpeg":
|
||||||
file = base_filename + ".jpg"
|
file = base_filename + ".jpg"
|
||||||
file_extension = ".jpg"
|
file_extension = ".jpg"
|
||||||
save_kwargs = {"quality": quality, "optimize": True}
|
save_kwargs = {"quality": quality, "optimize": True, "subsampling": jpeg_subsampling}
|
||||||
elif file_format == "webp":
|
elif file_format == "webp":
|
||||||
file = base_filename + ".webp"
|
file = base_filename + ".webp"
|
||||||
file_extension = ".webp"
|
file_extension = ".webp"
|
||||||
# Add optimization param to control performance
|
|
||||||
save_kwargs = {
|
save_kwargs = {
|
||||||
"quality": quality,
|
"quality": quality,
|
||||||
"lossless": lossless_webp,
|
"lossless": lossless_webp,
|
||||||
"method": 0,
|
"method": webp_method,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unsupported file format: {file_format}")
|
raise ValueError(f"Unsupported file format: {file_format}")
|
||||||
@@ -905,6 +924,8 @@ class SaveImageLM:
|
|||||||
extra_pnginfo=None,
|
extra_pnginfo=None,
|
||||||
lossless_webp=True,
|
lossless_webp=True,
|
||||||
quality=100,
|
quality=100,
|
||||||
|
webp_method=6,
|
||||||
|
jpeg_subsampling=0,
|
||||||
embed_workflow=False,
|
embed_workflow=False,
|
||||||
save_with_metadata=True,
|
save_with_metadata=True,
|
||||||
add_counter_to_filename=True,
|
add_counter_to_filename=True,
|
||||||
@@ -934,6 +955,8 @@ class SaveImageLM:
|
|||||||
extra_pnginfo,
|
extra_pnginfo,
|
||||||
lossless_webp,
|
lossless_webp,
|
||||||
quality,
|
quality,
|
||||||
|
webp_method,
|
||||||
|
jpeg_subsampling,
|
||||||
embed_workflow,
|
embed_workflow,
|
||||||
save_with_metadata,
|
save_with_metadata,
|
||||||
add_counter_to_filename,
|
add_counter_to_filename,
|
||||||
|
|||||||
@@ -363,3 +363,102 @@ def test_save_image_as_recipe_writes_recipe_without_async_scanner_calls(
|
|||||||
assert recipe["gen_params"] == {"prompt": "prompt text", "seed": 123}
|
assert recipe["gen_params"] == {"prompt": "prompt text", "seed": 123}
|
||||||
assert scanner._json_path_map[recipe["id"]] == os.path.normpath(str(recipe_files[0]))
|
assert scanner._json_path_map[recipe["id"]] == os.path.normpath(str(recipe_files[0]))
|
||||||
assert scanner.fts_updates == [(recipe["id"], "add")]
|
assert scanner.fts_updates == [(recipe["id"], "add")]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests for webp_method and jpeg_subsampling parameters
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _capture_save_kwargs(monkeypatch):
|
||||||
|
"""Monkeypatch Image.Image.save to capture kwargs while still saving to disk."""
|
||||||
|
real_save = Image.Image.save
|
||||||
|
captured_kwargs = {}
|
||||||
|
|
||||||
|
def _fake_save(self, fp, *args, **kwargs):
|
||||||
|
captured_kwargs.update(kwargs)
|
||||||
|
return real_save(self, fp, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(Image.Image, "save", _fake_save)
|
||||||
|
return captured_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def test_webp_method_default_passed_to_pillow_save(monkeypatch, tmp_path):
|
||||||
|
_configure_save_paths(monkeypatch, tmp_path)
|
||||||
|
_configure_metadata(monkeypatch, {"prompt": "test", "seed": 1})
|
||||||
|
captured = _capture_save_kwargs(monkeypatch)
|
||||||
|
|
||||||
|
node = SaveImageLM()
|
||||||
|
node.save_images([_make_image()], "ComfyUI", "webp", id="node-1")
|
||||||
|
|
||||||
|
assert "method" in captured
|
||||||
|
assert captured["method"] == 6
|
||||||
|
|
||||||
|
|
||||||
|
def test_webp_method_custom_value_passed_to_pillow_save(monkeypatch, tmp_path):
|
||||||
|
_configure_save_paths(monkeypatch, tmp_path)
|
||||||
|
_configure_metadata(monkeypatch, {"prompt": "test", "seed": 1})
|
||||||
|
captured = _capture_save_kwargs(monkeypatch)
|
||||||
|
|
||||||
|
node = SaveImageLM()
|
||||||
|
node.save_images(
|
||||||
|
[_make_image()], "ComfyUI", "webp", id="node-1", webp_method=3
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["method"] == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_jpeg_subsampling_default_passed_to_pillow_save(monkeypatch, tmp_path):
|
||||||
|
_configure_save_paths(monkeypatch, tmp_path)
|
||||||
|
_configure_metadata(monkeypatch, {"prompt": "test", "seed": 1})
|
||||||
|
captured = _capture_save_kwargs(monkeypatch)
|
||||||
|
|
||||||
|
node = SaveImageLM()
|
||||||
|
node.save_images([_make_image()], "ComfyUI", "jpeg", id="node-1")
|
||||||
|
|
||||||
|
assert "subsampling" in captured
|
||||||
|
assert captured["subsampling"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_jpeg_subsampling_custom_value_passed_to_pillow_save(monkeypatch, tmp_path):
|
||||||
|
_configure_save_paths(monkeypatch, tmp_path)
|
||||||
|
_configure_metadata(monkeypatch, {"prompt": "test", "seed": 1})
|
||||||
|
captured = _capture_save_kwargs(monkeypatch)
|
||||||
|
|
||||||
|
node = SaveImageLM()
|
||||||
|
node.save_images(
|
||||||
|
[_make_image()], "ComfyUI", "jpeg", id="node-1", jpeg_subsampling=1
|
||||||
|
)
|
||||||
|
|
||||||
|
assert captured["subsampling"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
class TestParameterDefaultConsistency:
|
||||||
|
"""Verify defaults match across INPUT_TYPES, save_images(), and process_image()."""
|
||||||
|
|
||||||
|
def test_webp_method_defaults_are_consistent(self):
|
||||||
|
input_types = SaveImageLM.INPUT_TYPES()
|
||||||
|
optional = input_types["optional"]
|
||||||
|
|
||||||
|
assert optional["webp_method"][1]["default"] == 6
|
||||||
|
assert SaveImageLM.save_images.__defaults__[4] == 6 # positional: webp_method=6 is at index 4
|
||||||
|
assert SaveImageLM.process_image.__defaults__[6] == 6
|
||||||
|
|
||||||
|
def test_jpeg_subsampling_defaults_are_consistent(self):
|
||||||
|
input_types = SaveImageLM.INPUT_TYPES()
|
||||||
|
optional = input_types["optional"]
|
||||||
|
|
||||||
|
assert optional["jpeg_subsampling"][1]["default"] == 0
|
||||||
|
assert SaveImageLM.save_images.__defaults__[5] == 0
|
||||||
|
assert SaveImageLM.process_image.__defaults__[7] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_png_does_not_pass_webp_method_or_jpeg_subsampling(monkeypatch, tmp_path):
|
||||||
|
_configure_save_paths(monkeypatch, tmp_path)
|
||||||
|
_configure_metadata(monkeypatch, {"prompt": "test", "seed": 1})
|
||||||
|
captured = _capture_save_kwargs(monkeypatch)
|
||||||
|
|
||||||
|
node = SaveImageLM()
|
||||||
|
node.save_images([_make_image()], "ComfyUI", "png", id="node-1")
|
||||||
|
|
||||||
|
assert "method" not in captured
|
||||||
|
assert "subsampling" not in captured
|
||||||
|
|||||||
@@ -130,6 +130,35 @@ app.registerExtension({
|
|||||||
widget.serializeValue = () => {
|
widget.serializeValue = () => {
|
||||||
return applyTextReplacements(widget.value);
|
return applyTextReplacements(widget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Conditional widget visibility for webp_method / jpeg_subsampling ---
|
||||||
|
const formatWidget = getWidgetByName(this, "file_format");
|
||||||
|
const webpMethodWidget = getWidgetByName(this, "webp_method");
|
||||||
|
const jpegSubWidget = getWidgetByName(this, "jpeg_subsampling");
|
||||||
|
|
||||||
|
function updateFormatConditional() {
|
||||||
|
const fmt = formatWidget?.value;
|
||||||
|
if (webpMethodWidget) {
|
||||||
|
webpMethodWidget.disabled = fmt !== "webp";
|
||||||
|
webpMethodWidget.hidden = fmt !== "webp";
|
||||||
|
}
|
||||||
|
if (jpegSubWidget) {
|
||||||
|
jpegSubWidget.disabled = fmt !== "jpeg";
|
||||||
|
jpegSubWidget.hidden = fmt !== "jpeg";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial state
|
||||||
|
updateFormatConditional();
|
||||||
|
|
||||||
|
// Watch for format changes
|
||||||
|
if (formatWidget) {
|
||||||
|
const origCallback = formatWidget.callback;
|
||||||
|
formatWidget.callback = function (value) {
|
||||||
|
origCallback?.call(this, value);
|
||||||
|
updateFormatConditional();
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user