Files
IxMxAMAR 3f9e39bb44 v0.3.0: security hardening, audit fixes, 10 new ops, theme UX (matchMedia perf fix + sandbox + leaks + features)
Critical security fixes
-----------------------
- Sandbox Jinja2 in template_render + prompt_template_render (was RCE via shared workflows)
- Drop user home dir from filesystem allow-list (was arbitrary delete/overwrite)
- Confine save_image_with_manifest + sanitize filename basename
- SSRF block on http_get/http_post/notify_webhook (catches 169.254.169.254 metadata)
- Case-insensitive path confinement on Windows

Critical correctness/perf
-------------------------
- halftone_dots: O(N^2) -> O(cells*36); was freezing worker on 1024+ images
- Preserve latent dict keys (noise_mask, batch_index) in latent_math/noise/upscale
- Preserve alpha channel in invert + threshold_binary on RGBA inputs
- Force odd kernel in mask_erode/mask_dilate to prevent H+1 shape mismatch
- Snap aspect_ratio_pick output to multiples of 8 (was crashing SD UNet)

Theme engine (JS)
-----------------
- Hoist window.matchMedia() out of per-frame hot path -> recovers 200 FPS from 25
- Wrap-once draw method to stop stomping other extensions wrappers
- nodeRemoved hook + WeakMap groups + in-place canvas reuse -> three memory leaks closed
- Wire thickness now uses connections_width (the wrap was a no-op vs LiteGraph)
- onGroupAdded auto-themes new groups; budget change triggers redraw
- Gradient caching in synthwave/glassmorphic

New ops (10)
------------
- json_path (programming): dotted/bracketed path extract
- latent_nan_guard, latent_pad_crop (mask_latent)
- mask_from_depth, mask_invert (mask_latent)
- color_match_histogram, image_composite_over (image_pro/value)
- frequency_separation (image_pro/style)
- pad_to_multiple (image_pro/spatial)
- save_images_zip (io_workflow)
- llm_enhance_prompt via Ollama / LM Studio / llama.cpp (prompt)

Theme UX
--------
- Right-click "Theme..." submenu on every node (including vanilla ComfyUI nodes)
- Auto-contrast title text (WCAG AA-based flip)

Tests: 373 Python + 33 JS = 406 passing (added 33 new tests for security + features).
Full review notes in CHANGELOG.md.
2026-05-17 15:41:19 +05:30

116 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Resolution and aspect-ratio helpers."""
import math
from .. import op
_RATIO_PRESETS = {
"1:1 (square)": (1, 1),
"4:3": (4, 3),
"3:2": (3, 2),
"16:9": (16, 9),
"21:9": (21, 9),
"9:16 (portrait)": (9, 16),
"3:4 (portrait)": (3, 4),
"2:3 (portrait)": (2, 3),
}
_SDXL_BUCKETS = [
(1024, 1024),
(1152, 896),
(896, 1152),
(1216, 832),
(832, 1216),
(1344, 768),
(768, 1344),
(1536, 640),
(640, 1536),
]
@op(
op_id="aspect_ratio_pick",
display_name="Aspect Ratio Pick",
category="Resolution",
input_schema={"required": {
"preset": (list(_RATIO_PRESETS.keys()), {"default": "16:9"}),
"long_side": ("INT", {"default": 1024, "min": 64, "max": 8192}),
}},
output_indices=(5, 6, 8),
description="Returns width (out:5) and height (out:6) for the chosen ratio scaled to long_side.",
)
def aspect_ratio_pick(self, preset, long_side=1024):
rw, rh = _RATIO_PRESETS.get(preset, (1, 1))
if rw >= rh:
w = int(long_side)
h = int(round(long_side * rh / rw))
else:
h = int(long_side)
w = int(round(long_side * rw / rh))
# Snap to multiples of 8 — SD UNet/VAE require this (typically /8 or /64).
# Without snapping, long_side=1000 + 16:9 produced 1000×562 which crashed
# the model.
w = max(8, (w // 8) * 8)
h = max(8, (h // 8) * 8)
info = {"preset": preset, "ratio": [rw, rh], "width": w, "height": h, "snapped_to": 8}
return (w, h, info)
@op(
op_id="sdxl_bucket_pick",
display_name="SDXL Bucket Pick (9 official sizes)",
category="Resolution",
input_schema={"required": {"index": ("INT", {"default": 0, "min": 0, "max": 8})}},
output_indices=(5, 6, 8),
description="Pick one of the 9 official SDXL training resolutions by index.",
)
def sdxl_bucket_pick(self, index=0):
w, h = _SDXL_BUCKETS[int(index) % len(_SDXL_BUCKETS)]
return (w, h, {"index": int(index), "width": w, "height": h})
@op(
op_id="snap_to_multiple",
display_name="Snap to Multiple",
category="Resolution",
input_schema={"required": {
"value": ("INT", {"default": 1024}),
"multiple": ("INT", {"default": 8, "min": 1, "max": 1024}),
}},
output_indices=(5,),
description="Round `value` to the nearest multiple of `multiple`.",
)
def snap_to_multiple(self, value, multiple=8):
m = max(1, int(multiple))
return (int(round(int(value) / m) * m),)
@op(
op_id="megapixel_calculator",
display_name="Megapixel Calculator (target MP + ratio)",
category="Resolution",
input_schema={"required": {
"target_megapixels": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 64.0}),
"ratio_w": ("INT", {"default": 16, "min": 1, "max": 999}),
"ratio_h": ("INT", {"default": 9, "min": 1, "max": 999}),
"snap_multiple": ("INT", {"default": 8, "min": 1, "max": 1024}),
}},
output_indices=(5, 6, 8),
description="Find W,H such that W*H ≈ target_megapixels * 1e6 with given ratio, snapped to multiple.",
)
def megapixel_calculator(self, target_megapixels, ratio_w, ratio_h, snap_multiple=8):
target_pixels = float(target_megapixels) * 1_000_000
ratio_w, ratio_h = int(ratio_w), int(ratio_h)
# W = sqrt(target * ratio_w / ratio_h); H = W * ratio_h / ratio_w
w = math.sqrt(target_pixels * ratio_w / ratio_h)
h = w * ratio_h / ratio_w
m = max(1, int(snap_multiple))
w_snapped = int(round(w / m) * m)
h_snapped = int(round(h / m) * m)
return (w_snapped, h_snapped, {
"width": w_snapped, "height": h_snapped,
"actual_megapixels": round((w_snapped * h_snapped) / 1_000_000, 4),
})