From 2fd419a696586f9dc1fdf6a90b5ee2f81ca64e19 Mon Sep 17 00:00:00 2001 From: poxular Date: Mon, 1 Jun 2026 17:25:25 +0200 Subject: [PATCH] Fix preview crop: use PNG natural dimensions for aspect and layout. Co-authored-by: Cursor --- js/magicmatch_live.js | 52 ++++++++++++++++++++++------------- magicmatch_core/live_cache.py | 11 ++++---- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/js/magicmatch_live.js b/js/magicmatch_live.js index a2cb906..74b7d33 100644 --- a/js/magicmatch_live.js +++ b/js/magicmatch_live.js @@ -75,6 +75,8 @@ class LivePreviewPanel { this.uLut = null; this.displayW = 200; this.displayH = 200; + this.imgW = 0; + this.imgH = 0; this.aspect = 1; this._glBw = 0; this._glBh = 0; @@ -82,7 +84,7 @@ class LivePreviewPanel { this.wrap = document.createElement("div"); this.wrap.style.cssText = - "width:100%;margin-top:6px;overflow:hidden;box-sizing:border-box;"; + "width:100%;margin-top:6px;overflow:visible;box-sizing:border-box;"; this.hint = document.createElement("div"); this.hint.textContent = "Run workflow once → live slider preview"; @@ -183,23 +185,25 @@ class LivePreviewPanel { return true; } - /** CSS display size — uses node width and height so portrait fills tall nodes. */ + /** CSS display size from decoded image aspect (width / height). */ layoutPreview(nodeWidth, nodeHeight) { + const aspect = this.imgW > 0 && this.imgH > 0 ? this.imgW / this.imgH : this.aspect; + if (!aspect || aspect <= 0) return [nodeWidth, 200]; + const pad = 16; const availW = Math.max(MIN_PREVIEW_PX, Math.floor(nodeWidth - pad)); - const availH = nodeHeight - ? Math.max(MIN_PREVIEW_PX, Math.floor(nodeHeight - NODE_CHROME_H)) - : null; - let w = availW; - let h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect)); + let h = Math.max(MIN_PREVIEW_PX, Math.round(w / aspect)); - if (availH && h < availH) { - h = availH; - w = Math.max(MIN_PREVIEW_PX, Math.round(h * this.aspect)); - if (w > availW) { - w = availW; - h = Math.max(MIN_PREVIEW_PX, Math.round(w / this.aspect)); + if (nodeHeight) { + const availH = Math.max(MIN_PREVIEW_PX, Math.floor(nodeHeight - NODE_CHROME_H)); + if (availH > h) { + h = availH; + w = Math.max(MIN_PREVIEW_PX, Math.round(h * aspect)); + if (w > availW) { + w = availW; + h = Math.max(MIN_PREVIEW_PX, Math.round(w / aspect)); + } } } @@ -211,11 +215,18 @@ class LivePreviewPanel { } /** Backing store = cache pixels × DPR (only changes when cache size changes). */ + sourceDimensions() { + const w = this.imgW > 0 ? this.imgW : this.cache?.w ?? 1; + const h = this.imgH > 0 ? this.imgH : this.cache?.h ?? 1; + return { w, h }; + } + syncGlBacking() { if (!this.cache) return false; + const { w: srcW, h: srcH } = this.sourceDimensions(); const dpr = window.devicePixelRatio || 1; - const bw = Math.max(1, Math.round(this.cache.w * dpr)); - const bh = Math.max(1, Math.round(this.cache.h * dpr)); + const bw = Math.max(1, Math.round(srcW * dpr)); + const bh = Math.max(1, Math.round(srcH * dpr)); if (bw === this._glBw && bh === this._glBh && this.gl) return false; this._glBw = bw; this._glBh = bh; @@ -266,8 +277,7 @@ class LivePreviewPanel { uploadSource(gl) { if (!this._img || !this.cache) return; - const srcW = this.cache.w; - const srcH = this.cache.h; + const { w: srcW, h: srcH } = this.sourceDimensions(); this._srcCanvas.width = srcW; this._srcCanvas.height = srcH; const ctx = this._srcCanvas.getContext("2d"); @@ -402,7 +412,11 @@ app.registerExtension({ panel.wrap.appendChild(panel.canvas); domWidget.computeSize = function (width) { - if (panel.cache) { + if (panel.imgW > 0 && panel.imgH > 0) { + return panel.layoutPreview(width, panel.node.size?.[1]); + } + if (panel.cache?.w && panel.cache?.h) { + panel.aspect = panel.cache.w / panel.cache.h; return panel.layoutPreview(width, panel.node.size?.[1]); } return [width, 200]; @@ -411,7 +425,7 @@ app.registerExtension({ const origOnResize = node.onResize; node.onResize = function (size) { const res = origOnResize?.apply(this, arguments); - if (panel.cache) { + if (panel.imgW > 0) { panel.layoutPreview(size[0], size[1]); panel.drawPreview(panel.getStrength()); } diff --git a/magicmatch_core/live_cache.py b/magicmatch_core/live_cache.py index bd29c26..f0a9364 100644 --- a/magicmatch_core/live_cache.py +++ b/magicmatch_core/live_cache.py @@ -36,18 +36,17 @@ def pack_live_cache(source_hwc: np.ndarray, merged_lut: np.ndarray) -> dict: from PIL import Image small = _downscale_hwc(source_hwc, LIVE_PREVIEW_MAX_EDGE) - sh, sw, _ = small.shape merged = np.asarray(merged_lut, dtype=np.float32).reshape(-1) + arr_u8 = (np.clip(small, 0, 1) * 255.0).astype(np.uint8) + pil = Image.fromarray(arr_u8, "RGB") buf = io.BytesIO() - Image.fromarray((np.clip(small, 0, 1) * 255.0).astype(np.uint8), "RGB").save( - buf, format="PNG", optimize=True - ) + pil.save(buf, format="PNG", optimize=True) return { "src_png": base64.b64encode(buf.getvalue()).decode("ascii"), - "w": int(sw), - "h": int(sh), + "w": int(pil.width), + "h": int(pil.height), "lut": base64.b64encode(merged.tobytes()).decode("ascii"), "lut_len": int(merged.size), "lut_size": 25,