사진크기변경
딥러닝 HDR Resize (옵션 0, 역광 보정만 + 필터 저장)미리보기이미지 선택 (여러 장 가능, 미리보기는 첫 장 기준):새 이미지 너비(px)쉐도우 톤 (-200~200)하이라이트 톤 (-200~200)노출 (-200~200)대비 (-200~200)채도 (-200~200)생동감 (-200~200)선명도 (-200~200)하이라이트 보호 (0~100)역광 보정 (±200%, 25% 단위) -200%-175%-150%-125%-100%-75%-50%-25%0%+25%+50%+75%+100%+125%+150%+175%+200%필터 이름필터 저장저장된 필터이미지 처리 및 다운로드'; return; } keys.forEach(name=>{ const row = document.createElement('div'); row.className = 'filter-item'; row.innerHTML = `${name}적용 삭제`; box.appendChild(row); }); } function applyFilter(name){ const all = JSON.parse(localStorage.getItem('savedFilters') || '{}'); const f = all[name]; if (!f){ alert('필터를 찾을 수 없습니다.'); return; } document.getElementById('width').value = f.width ?? 1280; document.getElementById('hdr_shadow_tone').value = f.hdr_shadow_tone ?? 0; document.getElementById('hdr_highlight_tone').value = f.hdr_highlight_tone ?? 0; document.getElementById('hdr_exposure').value = f.hdr_exposure ?? 0; document.getElementById('hdr_contrast').value = f.hdr_contrast ?? 0; document.getElementById('hdr_saturation').value = f.hdr_saturation ?? 0; document.getElementById('hdr_vibrancy').value = f.hdr_vibrancy ?? 0; document.getElementById('hdr_sharpness').value = f.hdr_sharpness ?? 0; document.getElementById('highlight_protect').value = f.highlight_protect ?? 0; document.getElementById('backlight_comp').value = f.backlight_comp ?? 0; alert(`'${name}' 필터가 적용되었습니다.`); renderPreviewDebounced(); } function deleteFilter(name){ const all = JSON.parse(localStorage.getItem('savedFilters') || '{}'); if (!all[name]) return; delete all[name]; localStorage.setItem('savedFilters', JSON.stringify(all)); loadFilters(); } /* ========= 속도 개선 유틸 (업/다운샘플 + 적분영상) ========= */ function downsampleNNFloat(src, w0, h0, w1, h1){ const dst = new Float32Array(w1*h1); const sx = w0 / w1, sy = h0 / h1; for (let y=0; y1)v=1; hist[(v*255)|0]++; } const target=Math.max(0,Math.min(N-1,Math.floor(p*N))); let acc=0; for(let b=0;btarget) return b/255; } return 1; } /* ========= 각 옵션 독립 처리 ========= */ /* 0) 역광 보정 — Fast Guided Filter 기반 (엣지/텍스처 보존, 고속) */ function applyBacklightCompEP(data, w, h, comp){ if(comp===0) return; const N = w*h; const Y = new Float32Array(N); for(let i=0,j=0;i0 ? 1.15 : 1.0); const gainClamp = g => Math.max(0.35, Math.min(2.8, g)); for(let i=0,j=0;i1) t=1; const wDark = t*t*(3-2*t); // 디테일 큰 곳(잔디, 질감)은 증폭 억제 const det = Math.abs(D[j] - 1); const wDet = 1/(1 + 8*det); const gain = gainClamp(1 + baseK*wDark*wDet); // 베이스만 증폭 → 재합성 const yNew = Math.max(0, Math.min(1, (yb*gain) * D[j])); const sRGB = (y>1e-6) ? (yNew / y) : 1; // RGB 동비율 → 색 보존 let rr = (data[i] /255)*sRGB; let gg = (data[i+1]/255)*sRGB; let bb = (data[i+2]/255)*sRGB; data[i] = Math.round(clamp01(rr)*255); data[i+1] = Math.round(clamp01(gg)*255); data[i+2] = Math.round(clamp01(bb)*255); } } /* 1) 쉐도우 톤 */ function applyShadows(data, shadowRaw){ if (shadowRaw === 0) return; const sAmt = shadowRaw / 200; const thresh = 0.6, power = 1.2; for (let i=0;i 1e-6) ? (Y2 / Y) : 1 + lift; r*=scale; g*=scale; b*=scale; data[i]=Math.round(clamp01(r)*255); data[i+1]=Math.round(clamp01(g)*255); data[i+2]=Math.round(clamp01(b)*255); } } } /* 2) 하이라이트 톤 */ function applyHighlights(data, highlightRaw){ if (highlightRaw === 0) return; const hAmt = -highlightRaw / 200; const thresh = 0.6, power = 1.2; for (let i=0;i thresh){ const t = (Y - thresh) / (1 - thresh); const comp = hAmt * Math.pow(t, power) * 0.5; const Y2 = clamp01(Y - comp); const scale = (Y > 1e-6) ? (Y2 / Y) : 1 - comp; r*=scale; g*=scale; b*=scale; data[i]=Math.round(clamp01(r)*255); data[i+1]=Math.round(clamp01(g)*255); data[i+2]=Math.round(clamp01(b)*255); } } } /* 3) 노출 */ function applyExposure(data, expMul){ if (expMul === 1) return; for (let i=0;i 1e-6) ? (Yc / Y) : 1; r*=scale; g*=scale; b*=scale; data[i]=Math.round(clamp01(r)*255); data[i+1]=Math.round(clamp01(g)*255); data[i+2]=Math.round(clamp01(b)*255); } } /* 5) 채도 */ function applySaturation(data, sMul){ if (sMul === 1) return; for (let i=0;i threshold){ const factor = Math.pow((Y - threshold) * invSpan, gamma); const Y2 = threshold + factor * (1 - threshold); const scale = (Y > 1e-6) ? (Y2 / Y) : 1; r*=scale; g*=scale; b*=scale; data[i]=Math.round(clamp01(r)*255); data[i+1]=Math.round(clamp01(g)*255); data[i+2]=Math.round(clamp01(b)*255); } } } /* 8) 선명도 */ function applySharpness(ctx, w, h, amount){ if (amount === 0) return; const a = Math.max(-2, Math.min(2, amount/100)); // -2..2 const src = ctx.getImageData(0, 0, w, h); const dst = ctx.createImageData(w, h); const s = src.data, d = dst.data; const get = (x,y,c)=>{ if (x<0) x=0; else if (x>=w) x=w-1; if (y<0) y=0; else if (y>=h) y=h-1; return s[(y*w + x)*4 + c]; }; const c00=0, c01=-a, c02=0, c10=-a, c11=1+4*a, c12=-a, c20=0, c21=-a, c22=0; for (let y=0; y { const img = new Image(); img.onload = () => { const { resultCanvas, newWidth, newHeight } = processImageBitmap(img, desiredWidth, rest); imgEl.src = resultCanvas.toDataURL("image/jpeg", 0.92); metaEl.textContent = `${file.name} • ${newWidth}×${newHeight}px (미리보기)`; }; img.src = reader.result; }; reader.readAsDataURL(file); } const renderPreviewDebounced = debounce(renderPreview, 120); /* ========= 다운로드(여러 장) ========= */ async function processAndDownload() { const files = document.getElementById("image-upload").files; if (!files || files.length === 0) { alert("이미지를 선택하세요."); return; } const { desiredWidth, ...rest } = gatherOptions(); for (const file of files) { await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const { resultCanvas, newWidth, newHeight } = processImageBitmap(img, desiredWidth, rest); resultCanvas.toBlob(blob => { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `Re_${file.name.split('.').slice(0,-1).join('.') || file.name}.jpg`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // 첫 파일은 제출 직후 미리보기 동기화 if (file === files[0]) { document.getElementById("preview-image").src = resultCanvas.toDataURL("image/jpeg", 0.92); document.getElementById("preview-meta").textContent = `${file.name} • ${newWidth}×${newHeight}px (미리보기)`; } resolve(); }, "image/jpeg", 0.98); }; img.onerror = reject; img.src = reader.result; }; reader.onerror = reject; reader.readAsDataURL(file); }); } alert("모든 이미지 처리가 완료되었습니다."); } /* ========= 초기 바인딩 ========= */ window.addEventListener('load', () => { loadFilters(); // 필터 목록 초기 로딩 // 파일/옵션 변경 시 미리보기(1장) 즉시 갱신 document.getElementById("image-upload").addEventListener('change', renderPreview); [ 'width','hdr_shadow_tone','hdr_highlight_tone','hdr_exposure', 'hdr_contrast','hdr_saturation','hdr_vibrancy','hdr_sharpness', 'highlight_protect','backlight_comp' ].forEach(id=>{ const el = document.getElementById(id); el.addEventListener('input', renderPreviewDebounced); if (el.tagName === 'SELECT') el.addEventListener('change', renderPreviewDebounced); }); });