1/** 2 * Misc AI-tool wiring â the three AI tools that don't share the 3 * inpaint pipeline: 4 * 5 * Harmonize: Reinhard color transfer on a body mask (no AI redraw) 6 * + an optional narrow inpaint on a seam mask if the 7 * "Seam fix" slider > 0. 8 * Canvas 2Ã/4Ã Upscale: in-browser bicubic resampling, no server. 9 * AI Upscale: Real-ESRGAN via /api/image/upscale-local. 10 * Style Transfer: img2img via /api/gallery/style-transfer. 11 * 12 * Plus the small `_addEmptyLayer` helper and its toolbar wiring, 13 * since it lived next to these. 14 * 15 * @param {{ 16 * apiBase: string, 17 * buildLayerBodyMask: (featherPx: number) => string | null, 18 * buildSeamMask: (featherPx: number) => string | null, 19 * applyImageTool: (endpoint, payload, layerName, btn, opts?) => Promise<void>, 20 * flatten: () => HTMLCanvasElement, 21 * saveState: (label?: string) => void, 22 * fitZoom: () => void, 23 * composite: () => void, 24 * createLayer: (name, w, h) => object, 25 * renderLayerPanel: () => void, 26 * spinnerModule: object, 27 * uiModule: object, 28 * }} deps 29 * 30 * @returns {{ addEmptyLayer: () => void }} 31 */ 32import { state } from './state.js'; 33 34export function wireAIToolsMisc({ 35 apiBase, buildLayerBodyMask, buildSeamMask, applyImageTool, 36 flatten, saveState, fitZoom, composite, createLayer, renderLayerPanel, 37 spinnerModule, uiModule, 38}) { 39 // ââ Harmonize sliders â Color match + Seam fix ââ 40 const harmColorPrev = document.getElementById('ge-harmonize-color-preview'); 41 const harmSeamPrev = document.getElementById('ge-harmonize-seam-preview'); 42 document.getElementById('ge-harmonize-color')?.addEventListener('input', (e) => { 43 document.getElementById('ge-harmonize-color-label').textContent = (e.target.value / 100).toFixed(2); 44 if (harmColorPrev) harmColorPrev.style.opacity = (parseInt(e.target.value, 10) / 100).toFixed(2); 45 }); 46 document.getElementById('ge-harmonize-seam')?.addEventListener('input', (e) => { 47 document.getElementById('ge-harmonize-seam-label').textContent = (e.target.value / 100).toFixed(2); 48 if (harmSeamPrev) harmSeamPrev.style.opacity = (parseInt(e.target.value, 10) / 100).toFixed(2); 49 }); 50 51 // Harmonize button â two-stage: 52 // 1) Reinhard color transfer on body mask (no AI redraw) 53 // 2) Optional narrow inpaint on seam mask (if seam_fix > 0) 54 document.getElementById('ge-harmonize-run')?.addEventListener('click', () => { 55 const prompt = document.getElementById('ge-harmonize-prompt')?.value?.trim() || 'photorealistic, natural lighting, seamless blend'; 56 const color_match = (parseInt(document.getElementById('ge-harmonize-color')?.value || '65')) / 100; 57 const seam_fix = (parseInt(document.getElementById('ge-harmonize-seam')?.value || '0')) / 100; 58 const bodyFeather = Math.max(6, Math.round(Math.min(state.imgWidth, state.imgHeight) * 0.012)); 59 const seamFeather = Math.max(8, Math.round(Math.min(state.imgWidth, state.imgHeight) * 0.015)); 60 const body_mask = buildLayerBodyMask(bodyFeather); 61 const seam_mask = seam_fix > 0.01 ? buildSeamMask(seamFeather) : null; 62 // Harmonize needs a non-base layer to color-match against the 63 // background. Without one, the server would fall back to legacy 64 // whole-image img2img â i.e. regenerate the whole photo. Block 65 // that and tell the user what's missing. 66 if (!body_mask) { 67 if (uiModule) uiModule.showToast('Harmonize needs a second layer pasted/imported over the base photo â nothing to color-match against.', 6000); 68 return; 69 } 70 const payload = { prompt, color_match, seam_fix, body_mask }; 71 if (seam_mask) payload.seam_mask = seam_mask; 72 applyImageTool('/api/image/harmonize', payload, 'Harmonized', document.getElementById('ge-harmonize-run')); 73 }); 74 75 // ââ Canvas upscale (bicubic) ââ 76 function canvasUpscale(factor) { 77 saveState(`Upscale ${factor}Ã`); 78 const newW = state.imgWidth * factor; 79 const newH = state.imgHeight * factor;
80 state.layers.forEach(l => { 81 const tmp = document.createElement('canvas'); 82 tmp.width = newW; tmp.height = newH; 83 const tCtx = tmp.getContext('2d'); 84 tCtx.imageSmoothingEnabled = true; 85 tCtx.imageSmoothingQuality = 'high'; 86 tCtx.drawImage(l.canvas, 0, 0, newW, newH); 87 l.canvas.width = newW; l.canvas.height = newH; 88 l.ctx.drawImage(tmp, 0, 0); 89 }); 90 if (state.maskCanvas) { state.maskCanvas.width = newW; state.maskCanvas.height = newH; } 91 state.imgWidth = newW; state.imgHeight = newH; 92 state.mainCanvas.width = newW; state.mainCanvas.height = newH; 93 const sizeLabel = document.getElementById('ge-canvas-size'); 94 if (sizeLabel) sizeLabel.textContent = `${newW}Ã${newH}`; 95 fitZoom(); 96 composite(); 97 uiModule.showToast(`Upscaled ${factor}à to ${newW}Ã${newH}`); 98 } 99 document.getElementById('ge-upscale-2x')?.addEventListener('click', () => canvasUpscale(2)); 100 document.getElementById('ge-upscale-4x')?.addEventListener('click', () => canvasUpscale(4)); 101 102 // ââ AI upscale (Real-ESRGAN, no diffusion server required) ââ 103 document.getElementById('ge-upscale-ai')?.addEventListener('click', async () => { 104 const btn = document.getElementById('ge-upscale-ai'); 105 const origHTML = btn.innerHTML; 106 btn.disabled = true; 107 let upWp = null; 108 try { 109 upWp = spinnerModule.createWhirlpool(14); 110 upWp.element.style.cssText = 'display:inline-block;vertical-align:middle;position:relative;top:1px;margin-right:6px;width:14px;height:14px;'; 111 btn.innerHTML = ''; 112 btn.appendChild(upWp.element); 113 const lbl = document.createElement('span'); 114 lbl.textContent = 'Upscalingâ¦'; 115 btn.appendChild(lbl); 116 } catch (_) { btn.textContent = 'Upscalingâ¦'; } 117 try { 118 const flat = flatten(); 119 const imageB64 = flat.toDataURL('image/png').split(',')[1]; 120 const res = await fetch('/api/image/upscale-local', { 121 method: 'POST', credentials: 'same-origin', 122 headers: { 'Content-Type': 'application/json' }, 123 body: JSON.stringify({ image: imageB64, scale: 2 }), 124 }); 125 if (!res.ok) throw new Error('Server returned ' + res.status); 126 const data = await res.json(); 127 if (data.image) { 128 const img = new Image(); 129 img.onload = () => { 130 if (!state.editorOpen) return; 131 saveState(); 132 const newW = img.width, newH = img.height; 133 const layer = createLayer('AI Upscaled', newW, newH); 134 layer.ctx.drawImage(img, 0, 0); 135 state.layers.push(layer); 136 state.activeLayerId = layer.id; 137 state.imgWidth = newW; state.imgHeight = newH; 138 state.mainCanvas.width = newW; state.mainCanvas.height = newH; 139 if (state.maskCanvas) { state.maskCanvas.width = newW; state.maskCanvas.height = newH; } 140 const sizeLabel = document.getElementById('ge-canvas-size'); 141 if (sizeLabel) sizeLabel.textContent = `${newW}Ã${newH}`; 142 fitZoom(); 143 composite(); 144 renderLayerPanel(); 145 uiModule.showToast(`AI upscaled to ${newW}Ã${newH}`); 146 }; 147 img.src = 'data:image/png;base64,' + data.image; 148 } else { 149 throw new Error(data.error || 'No image returned'); 150 } 151 } catch (e) { 152 uiModule.showToast('AI upscale failed: ' + e.message); 153 } 154 try { upWp?.destroy(); } catch (_) {} 155 btn.disabled = false; 156 btn.innerHTML = origHTML; 157 }); 158 159 // ââ Style transfer ââ 160 document.getElementById('ge-style-strength')?.addEventListener('input', (e) => { 161 document.getElementById('ge-style-strength-label').textContent = (parseInt(e.target.value) / 100).toFixed(2); 162 }); 163 document.getElementById('ge-style-run')?.addEventListener('click', async () => { 164 const btn = document.getElementById('ge-style-run'); 165 const prompt = document.getElementById('ge-style-prompt').value.trim(); 166 if (!prompt) { uiModule.showToast('Enter a style prompt'); return; } 167 const strength = parseInt(document.getElementById('ge-style-strength').value) / 100; 168 btn.disabled = true; btn.textContent = 'Applying...'; 169 try { 170 const flat = flatten(); 171 const blob = await new Promise(r => flat.toBlob(r, 'image/png')); 172 const fd = new FormData(); 173 fd.append('image', blob, 'style.png'); 174 fd.append('prompt', prompt); 175 fd.append('strength', String(strength)); 176 const res = await fetch(`${apiBase}/api/gallery/style-transfer`, { method: 'POST', credentials: 'same-origin', body: fd }); 177 if (!res.ok) throw new Error('Server returned ' + res.status); 178 const data = await res.json(); 179 if (data.image) { 180 const img = new Image(); 181 img.onload = () => { 182 if (!state.editorOpen) return; 183 saveState(); 184 const layer = createLayer('Styled: ' + prompt.substring(0, 20), state.imgWidth, state.imgHeight); 185 layer.ctx.drawImage(img, 0, 0, state.imgWidth, state.imgHeight); 186 state.layers.push(layer); 187 state.activeLayerId = layer.id; 188 composite(); 189 renderLayerPanel(); 190 uiModule.showToast('Style applied'); 191 }; 192 img.src = 'data:image/png;base64,' + data.image; 193 } else { 194 throw new Error(data.error || 'No image returned'); 195 } 196 } catch (e) { 197 uiModule.showToast('Style transfer failed: ' + e.message); 198 } 199 btn.disabled = false; btn.textContent = 'Apply Style'; 200 }); 201 202 // ââ Add empty layer (used by the layer-panel header button + the 203 // Ctrl+Alt+J keyboard shortcut). Returned so keyboard-shortcuts.js 204 // can call it through the same path. ââ 205 function addEmptyLayer() { 206 saveState('Add layer'); 207 const layer = createLayer('Layer ' + state.layers.length, state.imgWidth, state.imgHeight); 208 state.layers.push(layer); 209 state.activeLayerId = layer.id; 210 renderLayerPanel(); 211 composite(); 212 } 213 document.getElementById('ge-add-layer')?.addEventListener('click', addEmptyLayer); 214 215 return { addEmptyLayer }; 216}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.