Image Sharpen

四种专业级锐化算法 · RCAS / Adaptive USM / High-Pass / 去卷积

拖拽图片到此处,或点击上传

支持批量上传 JPG / PNG / WebP

处理中...0%
(function(){ 'use strict'; /* ── i18n 系统 ── */ const i18n = { zh: { title:'Image Sharpen', subtitle:'四种专业级锐化算法 · RCAS / Adaptive USM / High-Pass / 去卷积', uploadText:'拖拽图片到此处,或点击上传', uploadHint:'支持批量上传 JPG / PNG / WebP', imageListPrefix:'图片列表', imageListSuffix:'张', clearAll:'清空列表', methodTitle:'锐化方法', strength:'强度', radius:'半径', threshold:'阈值', iter:'迭代', origPreview:'原始图片', resultPreview:'锐化后', origPlaceholder:'选择图片后显示预览', resultPlaceholder:'调整参数后点击「应用」预览', downloadCurrent:'下载当前', downloadAll:'锐化全部', addMore:'添加更多', reset:'重新开始', processing:'正在处理...', progressLabel:'处理中...', batchCount:'(0)', rcas_label:'RCAS', adaptive_usm_label:'Adaptive USM', high_pass_label:'High-Pass', deconv_label:'去卷积', rcas_desc:'AMD FSR 对比自适应锐化 — 5-tap 十字滤波器,几乎无伪影', adaptive_usm_desc:'自适应反锐化掩模 — 根据局部纹理密度调节锐化量', high_pass_desc:'高频增强 — 提取高频细节叠加回原图', deconv_desc:'Richardson-Lucy 迭代去卷积 — 可恢复失焦模糊', preset_low:'轻微锐化', preset_std:'标准锐化', preset_high:'强力锐化', preset_extreme:'极致锐化', preset_gentle:'轻微', preset_standard:'标准', preset_strong:'强力', preset_denoise:'降噪锐化', preset_texture:'纹理增强', preset_clear:'标准清晰', preset_glow:'柔和发光', preset_edge:'锐利边缘', preset_deblur_light:'轻微去模糊', preset_deblur_std:'标准去模糊', preset_deblur_strong:'强力去模糊', preset_deblur_restore:'极致修复', preset_quick:'快速预览', toast_success:'处理完成', toast_error:'处理失败', no_jszip:'JSZip 加载失败,部分功能不可用', file_error:'文件读取失败', image_loaded:'图片加载完成', cancel:'已取消' }, en: { title:'Image Sharpen', subtitle:'Four professional sharpening algorithms · RCAS / Adaptive USM / High-Pass / Deconv', uploadText:'Drag images here or click to upload', uploadHint:'Supports batch JPG / PNG / WebP', imageListPrefix:'Images', imageListSuffix:'', clearAll:'Clear All', methodTitle:'Sharpening Method', strength:'Strength', radius:'Radius', threshold:'Threshold', iter:'Iterations', origPreview:'Original', resultPreview:'Sharpened', origPlaceholder:'Select an image to preview', resultPlaceholder:'Adjust parameters and click Apply', downloadCurrent:'Download Current', downloadAll:'Sharpen All', addMore:'Add More', reset:'Start Over', processing:'Processing...', progressLabel:'Processing...', batchCount:'(0)', rcas_label:'RCAS', adaptive_usm_label:'Adaptive USM', high_pass_label:'High-Pass', deconv_label:'Deconvolution', rcas_desc:'AMD FSR contrast adaptive sharpening — 5-tap cross filter, near artifact-free', adaptive_usm_desc:'Adaptive Unsharp Mask — adjusts sharpening by local texture density', high_pass_desc:'High-frequency boost — extracts fine details and overlays', deconv_desc:'Richardson-Lucy iterative deconvolution — restores defocus blur', preset_low:'Light Sharpening', preset_std:'Standard', preset_high:'Strong', preset_extreme:'Extreme', preset_gentle:'Gentle', preset_standard:'Standard', preset_strong:'Strong', preset_denoise:'Denoise Sharpen', preset_texture:'Texture Boost', preset_clear:'Clear', preset_glow:'Soft Glow', preset_edge:'Sharp Edge', preset_deblur_light:'Light Deblur', preset_deblur_std:'Standard Deblur', preset_deblur_strong:'Strong Deblur', preset_deblur_restore:'Extreme Restore', preset_quick:'Quick Preview', toast_success:'Processing complete', toast_error:'Processing failed', no_jszip:'JSZip failed to load, some features unavailable', file_error:'File read error', image_loaded:'Image loaded', cancel:'Cancelled' } }; const userLang = navigator.language.startsWith('zh') ? 'zh' : 'en'; const lang = i18n[userLang] || i18n.en; const __ = (key) => lang[key] || i18n.zh[key] || key; /* ── 全局错误处理 ── */ window.addEventListener('error', function(e) { console.error('Global error:', e.message, e.filename, e.lineno); const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('toast_error') + ': ' + (e.message || 'Unknown'); toastEl.className = 'toast error show'; setTimeout(() => toastEl.classList.remove('show'), 4000); } return false; }); window.addEventListener('unhandledrejection', function(e) { console.error('Unhandled rejection:', e.reason); }); document.addEventListener('DOMContentLoaded', function() { /* ── i18n 替换静态元素 ── */ document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); el.textContent = __(key); }); /* 替换 method label 与 desc(在 METHODS 对象中) */ if (typeof METHODS !== 'undefined') { Object.keys(METHODS).forEach(k => { const m = METHODS[k]; const labelKey = k + '_label'; const descKey = k + '_desc'; if (__(labelKey) !== labelKey) m.label = __(labelKey); if (__(descKey) !== descKey) m.desc = __(descKey); }); } /* 重新渲染预设(触发一次) */ if (typeof renderPresets === 'function') { renderPresets(); } /* ── 补全 debouncePreview ── */ window.debouncePreview = function() { if (window._rafId) cancelAnimationFrame(window._rafId); window._rafId = requestAnimationFrame(() => { window._rafId = null; // 调用预览逻辑 if (typeof runPreview === 'function') runPreview(); else console.warn('runPreview not defined'); }); }; /* ── 补全 runPreview ── */ window.runPreview = function() { if (state.processing) return; const selected = state.images.find(img => img.id === state.selectedId); if (!selected) return; state.processing = true; const btn = document.getElementById('previewBtn'); if (btn) btn.disabled = true; const statusBar = document.getElementById('statusBar'); const statusText = document.getElementById('statusText'); if (statusBar && statusText) { statusText.textContent = __('processing'); statusBar.classList.add('show'); } setTimeout(() => { try { if (window._cancel) { window._cancel = false; state.processing = false; if (btn) btn.disabled = false; if (statusBar) statusBar.classList.remove('show'); return; } const imgData = selected.loadedData; if (!imgData) { throw new Error('No image data'); } const result = runSharpen(imgData, state.method, state.strength, state.radius, state.threshold, state.iter); state.pendingDownload = result; // 显示结果 const resultCanvas = document.getElementById('resultCanvas'); const resultPlaceholder = document.getElementById('resultPlaceholder'); if (resultCanvas) { resultCanvas.width = result.width; resultCanvas.height = result.height; resultCanvas.getContext('2d').putImageData(result, 0, 0); } if (resultPlaceholder) resultPlaceholder.style.display = 'none'; const resultDims = document.getElementById('resultDims'); if (resultDims) resultDims.textContent = result.width + '×' + result.height; if (statusBar) statusBar.classList.remove('show'); state.processing = false; if (btn) btn.disabled = false; } catch (err) { console.error('Preview error:', err); state.processing = false; if (btn) btn.disabled = false; const statusBar = document.getElementById('statusBar'); if (statusBar) statusBar.classList.remove('show'); const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('toast_error') + ': ' + err.message; toastEl.className = 'toast error show'; setTimeout(() => toastEl.classList.remove('show'), 4000); } } }, 100); }; /* ── 补全事件绑定(避免重复绑定) ── */ const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); if (uploadArea && fileInput) { uploadArea.addEventListener('click', function(e) { if (e.target.tagName !== 'INPUT') fileInput.click(); }); fileInput.addEventListener('change', function(e) { const files = e.target.files; if (!files || !files.length) return; handleFiles(files); }); uploadArea.addEventListener('dragover', function(e) { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', function(e) { e.preventDefault(); uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', function(e) { e.preventDefault(); uploadArea.classList.remove('dragover'); const files = e.dataTransfer.files; if (files && files.length) handleFiles(files); }); } /* ── handleFiles 补全(如果不存在) ── */ if (typeof window.handleFiles !== 'function') { window.handleFiles = function(files) { Array.from(files).forEach(file => { if (!file.type.match('image.*')) return; const reader = new FileReader(); reader.onerror = function(err) { console.error('FileReader error', err); const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('file_error') + ': ' + file.name; toastEl.className = 'toast error show'; setTimeout(() => toastEl.classList.remove('show'), 4000); } }; reader.onload = function(e) { const img = new Image(); img.onerror = function() { console.error('Image load error:', file.name); }; img.onload = function() { const id = ++window.idCounter; const canvas = document.createElement('canvas'); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const imgData = ctx.getImageData(0, 0, img.width, img.height); state.images.push({ id: id, name: file.name, width: img.width, height: img.height, loadedData: imgData }); if (state.selectedId === null) { state.selectedId = id; } updateImageList(); updateVisibility(); if (state.selectedId === id) { showPreview(id); } }; img.src = e.target.result; }; reader.readAsDataURL(file); }); }; } /* ── 补全 updateImageList ── */ if (typeof window.updateImageList !== 'function') { window.updateImageList = function() { const listBody = document.getElementById('imageListBody'); const countEl = document.getElementById('imageCount'); if (countEl) countEl.textContent = state.images.length; if (!listBody) return; listBody.innerHTML = ''; state.images.forEach(img => { const item = document.createElement('div'); item.className = 'image-item' + (img.id === state.selectedId ? ' active' : ''); item.dataset.id = img.id; const thumb = document.createElement('img'); thumb.className = 'image-item-thumb'; // create thumbnail from loadedData const tempC = document.createElement('canvas'); tempC.width = 92; tempC.height = 68; const tempCtx = tempC.getContext('2d'); tempCtx.drawImage( (function(){ const c = document.createElement('canvas'); c.width = img.loadedData.width; c.height = img.loadedData.height; c.getContext('2d').putImageData(img.loadedData, 0, 0); return c; })(), 0, 0, 92, 68 ); thumb.src = tempC.toDataURL(); thumb.alt = img.name; const info = document.createElement('div'); info.className = 'image-item-info'; const nameSpan = document.createElement('div'); nameSpan.className = 'image-item-name'; nameSpan.textContent = img.name; const dimsSpan = document.createElement('div'); dimsSpan.className = 'image-item-dims'; dimsSpan.textContent = img.width + '×' + img.height; info.appendChild(nameSpan); info.appendChild(dimsSpan); const removeBtn = document.createElement('button'); removeBtn.className = 'image-item-remove'; removeBtn.textContent = '×'; removeBtn.addEventListener('click', function(e) { e.stopPropagation(); state.images = state.images.filter(i => i.id !== img.id); if (state.selectedId === img.id) { state.selectedId = state.images.length ? state.images[0].id : null; } updateImageList(); updateVisibility(); if (state.selectedId !== null) { showPreview(state.selectedId); } else { clearPreview(); } }); item.appendChild(thumb); item.appendChild(info); item.appendChild(removeBtn); item.addEventListener('click', function() { state.selectedId = img.id; updateImageList(); showPreview(img.id); }); listBody.appendChild(item); }); }; } /* ── updateVisibility ── */ if (typeof window.updateVisibility !== 'function') { window.updateVisibility = function() { const hasImages = state.images.length > 0; const section = document.getElementById('imageListSection'); if (section) section.style.display = hasImages ? 'block' : 'none'; const controls = document.getElementById('controlsSection'); if (controls) controls.style.display = hasImages ? 'block' : 'none'; const preview = document.getElementById('previewSection'); if (preview) preview.style.display = (hasImages && state.selectedId !== null) ? 'grid' : 'none'; const actions = document.getElementById('actionsSection'); if (actions) actions.style.display = hasImages ? 'flex' : 'none'; const progress = document.getElementById('progressWrap'); if (progress) progress.classList.remove('show'); const cnt = document.getElementById('batchCount'); if (cnt) cnt.textContent = '(' + state.images.length + ')'; }; } /* ── showPreview ── */ if (typeof window.showPreview !== 'function') { window.showPreview = function(id) { const img = state.images.find(i => i.id === id); if (!img) return; const origCanvas = document.getElementById('origCanvas'); const origPlaceholder = document.getElementById('origPlaceholder'); if (origCanvas) { origCanvas.width = img.width; origCanvas.height = img.height; const ctx = origCanvas.getContext('2d'); ctx.putImageData(img.loadedData, 0, 0); } if (origPlaceholder) origPlaceholder.style.display = 'none'; const dims = document.getElementById('origDims'); if (dims) dims.textContent = img.width + '×' + img.height; // clear result const resultCanvas = document.getElementById('resultCanvas'); const resultPlaceholder = document.getElementById('resultPlaceholder'); if (resultCanvas) { resultCanvas.width = 0; resultCanvas.height = 0; } if (resultPlaceholder) resultPlaceholder.style.display = ''; const resultDims = document.getElementById('resultDims'); if (resultDims) resultDims.textContent = '—'; // auto-debounce preview if (typeof debouncePreview === 'function') debouncePreview(); }; } /* ── clearPreview ── */ if (typeof window.clearPreview !== 'function') { window.clearPreview = function() { const origCanvas = document.getElementById('origCanvas'); const resultCanvas = document.getElementById('resultCanvas'); const origPlaceholder = document.getElementById('origPlaceholder'); const resultPlaceholder = document.getElementById('resultPlaceholder'); if (origCanvas) { origCanvas.width = 0; origCanvas.height = 0; } if (resultCanvas) { resultCanvas.width = 0; resultCanvas.height = 0; } if (origPlaceholder) origPlaceholder.style.display = ''; if (resultPlaceholder) resultPlaceholder.style.display = ''; document.getElementById('origDims').textContent = '—'; document.getElementById('resultDims').textContent = '—'; state.pendingDownload = null; }; } /* ── 取消按钮支持 ── */ window._cancel = false; // 在预览按钮点击时设置取消标志 const previewBtn = document.getElementById('previewBtn'); if (previewBtn) { previewBtn.addEventListener('click', function() { if (state.processing) return; window._cancel = false; if (typeof runPreview === 'function') runPreview(); }); } /* ── 下载按钮 ── */ const downloadBtn = document.getElementById('downloadBtn'); if (downloadBtn) { downloadBtn.addEventListener('click', function() { if (!state.pendingDownload) return; const canvas = document.getElementById('resultCanvas'); if (!canvas) return; const link = document.createElement('a'); link.download = 'sharpened_' + (state.images.find(i => i.id === state.selectedId)?.name || 'image.png'); link.href = canvas.toDataURL('image/png'); link.click(); }); } /* ── 批量下载 ── */ const downloadAllBtn = document.getElementById('downloadAllBtn'); if (downloadAllBtn) { downloadAllBtn.addEventListener('click', function() { if (!window.JSZip) { const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('no_jszip'); toastEl.className = 'toast error show'; setTimeout(() => toastEl.classList.remove('show'), 4000); } return; } const zip = new JSZip(); let count = 0; const total = state.images.length; const progressFill = document.getElementById('progressFill'); const progressText = document.getElementById('progressText'); const progressPercent = document.getElementById('progressPercent'); const progressWrap = document.getElementById('progressWrap'); if (progressWrap) progressWrap.classList.add('show'); state.images.forEach((img, idx) => { setTimeout(() => { try { const result = runSharpen(img.loadedData, state.method, state.strength, state.radius, state.threshold, state.iter); const canvas = document.createElement('canvas'); canvas.width = result.width; canvas.height = result.height; canvas.getContext('2d').putImageData(result, 0, 0); const base64 = canvas.toDataURL('image/png').split(',')[1]; zip.file(img.name.replace(/\.(png|jpg|jpeg|webp)$/i, '_sharpened.png') || 'image.png', base64, {base64: true}); count++; if (progressFill) progressFill.style.width = ((count / total) * 100) + '%'; if (progressPercent) progressPercent.textContent = Math.round((count / total) * 100) + '%'; if (progressText) progressText.textContent = __('progressLabel') + ' ' + count + '/' + total; if (count === total) { zip.generateAsync({type:'blob'}).then(function(content) { const link = document.createElement('a'); link.download = 'sharpened_all.zip'; link.href = URL.createObjectURL(content); link.click(); URL.revokeObjectURL(link.href); if (progressWrap) progressWrap.classList.remove('show'); const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('toast_success'); toastEl.className = 'toast success show'; setTimeout(() => toastEl.classList.remove('show'), 3000); } }).catch(function(err) { console.error('Zip generation error:', err); if (progressWrap) progressWrap.classList.remove('show'); }); } } catch (err) { console.error('Batch sharpen error:', err); count++; if (progressFill) progressFill.style.width = ((count / total) * 100) + '%'; } }, idx * 200); }); }); } /* ── 添加更多 ── */ const addMoreBtn = document.getElementById('addMoreBtn'); if (addMoreBtn && fileInput) { addMoreBtn.addEventListener('click', function() { fileInput.click(); }); } /* ── 重置 ── */ const resetBtn = document.getElementById('resetBtn'); if (resetBtn) { resetBtn.addEventListener('click', function() { state.images = []; state.selectedId = null; state.pendingDownload = null; if (fileInput) fileInput.value = ''; updateImageList(); updateVisibility(); clearPreview(); window._cancel = true; }); } /* ── 清空列表 ── */ const clearAllBtn = document.getElementById('clearAllBtn'); if (clearAllBtn) { clearAllBtn.addEventListener('click', function() { state.images = []; state.selectedId = null; state.pendingDownload = null; if (fileInput) fileInput.value = ''; updateImageList(); updateVisibility(); clearPreview(); }); } /* ── 方法切换 ── */ document.querySelectorAll('.method-btn').forEach(btn => { btn.addEventListener('click', function() { const method = this.dataset.method; if (!method) return; state.method = method; document.querySelectorAll('.method-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); const m = METHODS[method]; if (m) { const desc = document.getElementById('methodDesc'); if (desc) desc.textContent = m.desc; state.strength = m.defaultStrength; state.radius = m.defaultRadius; state.threshold = m.defaultThreshold; state.iter = m.defaultIter; strengthSlider.value = m.defaultStrength * 100; strengthVal.textContent = m.defaultStrength.toFixed(2); radiusSlider.value = m.defaultRadius * 10; radiusVal.textContent = m.defaultRadius.toFixed(1); thresholdSlider.value = m.defaultThreshold * 100; thresholdVal.textContent = m.defaultThreshold.toFixed(2); iterSlider.value = m.defaultIter; iterVal.textContent = m.defaultIter; // show/hide slider groups const strengthGroup = document.getElementById('strengthGroup'); const radiusGroup = document.getElementById('radiusGroup'); const thresholdGroup = document.getElementById('thresholdGroup'); const iterGroup = document.getElementById('iterGroup'); if (strengthGroup) strengthGroup.style.display = ''; if (radiusGroup) radiusGroup.style.display = m.hasRadius ? '' : 'none'; if (thresholdGroup) thresholdGroup.style.display = m.hasThreshold ? '' : 'none'; if (iterGroup) iterGroup.style.display = m.hasIter ? '' : 'none'; renderPresets(); debouncePreview(); } }); }); /* ── 滑块事件 ── */ if (strengthSlider) { strengthSlider.addEventListener('input', function() { state.strength = this.value / 100; strengthVal.textContent = state.strength.toFixed(2); debouncePreview(); }); } if (radiusSlider) { radiusSlider.addEventListener('input', function() { state.radius = this.value / 10; radiusVal.textContent = state.radius.toFixed(1); debouncePreview(); }); } if (thresholdSlider) { thresholdSlider.addEventListener('input', function() { state.threshold = this.value / 100; thresholdVal.textContent = state.threshold.toFixed(2); debouncePreview(); }); } if (iterSlider) { iterSlider.addEventListener('input', function() { state.iter = parseInt(this.value, 10); iterVal.textContent = state.iter; debouncePreview(); }); } /* ── 初始化可见性 ── */ updateVisibility(); /* ── 检查 JSZip ── */ if (typeof JSZip === 'undefined') { const toastEl = document.getElementById('toast'); if (toastEl) { toastEl.textContent = __('no_jszip'); toastEl.className = 'toast error show'; setTimeout(() => toastEl.classList.remove('show'), 5000); } } }); })();