图片混淆 - 专业版
ddd
一、核心技术与性能优化
1. 技术核心:Gilbert 空间填充曲线
工具应用依然基于 Gilbert 2D 空间填充曲线算法,对图片像素进行可逆的重排与混淆。该算法从数学层面保障了混淆的可逆性,同时实现数据无损处理,确保图片还原后无任何质量损耗。
2. 多线程不卡顿:Web Worker
这是本次版本中最重要的性能升级。具体优化为:将生成曲线、像素遍历等耗时的图片计算操作,全部封装到 Web Worker 线程中。实际使用效果显著 —— 即使上传并处理上千万像素的高清大图,主界面的按钮点击、动画播放等操作也不会出现卡顿或假死,用户操作流畅度得到大幅提升。
3. 隐私安全保障:纯本地处理
所有图片相关操作(包括加载、处理、混淆与还原),均在浏览器本地内存中完成。数据不会上传至任何服务器,从数据流转的源头最大程度保护用户隐私安全。
二、页面美化与设计风格
1. 设计语言:高级玻璃拟态(Modern Glassmorphism)
移除工具中过于极客的元素,采用更优雅、专业的现代设计风格。页面卡片具备高透明度与磨砂玻璃质感(通过 backdrop-filter: blur() 技术实现),让整体界面更显轻盈,同时增强视觉层次感。
2. 视觉效果:柔和流光背景
在页面背景中加入柔和且动态的渐变流光效果,替代传统静态背景,避免视觉单调感,为用户提供更舒适、更具高级感的视觉体验。
3. 响应式布局
针对不同设备屏幕尺寸进行适配优化,无论是移动设备(手机、平板)还是桌面设备(电脑),都能保持一致的界面美观度与操作可用性,确保在各类场景下的使用体验不受影响。
三、新增功能与交互增强
1. 本地历史记录功能
自动保存最近 6 张上传或处理的图片记录,即使关闭浏览器后重新打开,记录也不会丢失,用户可随时点击历史记录快速加载对应图片。技术实现上,采用浏览器 IndexedDB(本地数据库)替代容量有限的 LocalStorage,专门适配大文件存储需求,确保历史记录稳定保存。
2. 原图快速对比功能
提供实时的混淆效果对比体验:在图片显示区域长按鼠标(或触摸屏幕)时,混淆图会即时切换为原图;松开鼠标(或离开屏幕)后,自动恢复为混淆状态。该功能通过 mousedown/mouseup 事件结合 Image 对象的 URL 切换实现,操作过程无延迟,流畅度极高。
3. 优雅消息提示功能
全面移除所有系统默认的 alert () 弹窗,改用顶部居中、自动消失的 Toast Notifications(吐司提示)。不仅提升了用户操作反馈的质量,还避免了弹窗对操作流程的打断,进一步优化界面美观度。
4. 拖拽上传支持功能
新增拖拽上传方式,用户可直接将图片文件拖入指定区域完成加载,无需手动点击 “选择文件” 按钮,大幅简化上传操作步骤,增强使用便捷性。
5. 一键还原与保存功能
新增独立的 “还原原图” 按钮与 “保存结果” 按钮:其中 “还原原图” 功能基于最初的 originalBlob 引用,确保还原的是未经处理的原始图片;“保存结果” 功能在保存文件时,会自动为文件名添加时间戳,避免文件覆盖问题。两个功能均无需寻找隐藏入口,操作更直接高效。
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>图片混淆 - 专业版</title> <style> :root { --bg-gradient-1: #4f46e5; --bg-gradient-2: #ec4899; --glass-bg: rgba(255, 255, 255, 0.7); --glass-border: rgba(255, 255, 255, 0.5); --text-primary: #1e293b; --text-secondary: #64748b; --accent: #4f46e5; --accent-hover: #4338ca; --danger: #ef4444; --radius-lg: 24px; --radius-md: 12px; --shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); } /* 深色模式适配 */ @media (prefers-color-scheme: dark) { :root { --glass-bg: rgba(30, 41, 59, 0.7); --glass-border: rgba(255, 255, 255, 0.1); --text-primary: #f8fafc; --text-secondary: #94a3b8; --accent: #6366f1; --accent-hover: #818cf8; } } * { margin: 0; padding: 0; box-sizing: border-box; outline: none; } body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; background: #0f172a; color: var(--text-primary); overflow-x: hidden; position: relative; } /* 动态流光背景 */ .bg-orb { position: fixed; border-radius: 50%; filter: blur(80px); z-index: -1; animation: float 10s infinite ease-in-out; opacity: 0.6; } .orb-1 { width: 400px; height: 400px; background: var(--bg-gradient-1); top: -100px; left: -100px; } .orb-2 { width: 300px; height: 300px; background: var(--bg-gradient-2); bottom: -50px; right: -50px; animation-delay: -5s; } @keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, 50px); } } /* 主容器 */ .container { width: 100%; max-width: 900px; padding: 2rem 1.5rem; z-index: 1; } header { text-align: center; margin-bottom: 2.5rem; } h1 { font-size: 2.2rem; font-weight: 700; margin-bottom: 0.5rem; letter-spacing: -0.025em; } .suBTitle { color: var(--text-secondary); font-size: 0.95rem; max-width: 500px; margin: 0 auto; line-height: 1.5; } /* 卡片风格 */ .card { background: var(--glass-bg); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid var(--glass-border); border-radius: var(--radius-lg); padding: 2rem; box-shadow: var(--shadow); transition: transform 0.3s ease; } /* 图片预览区 */ .preview-box { width: 100%; min-height: 350px; border: 2px dashed var(--glass-border); border-radius: var(--radius-md); background: rgba(0,0,0,0.05); display: flex; justify-content: center; align-items: center; position: relative; overflow: hidden; cursor: pointer; transition: all 0.3s ease; margin-bottom: 2rem; } .preview-box:hover { background: rgba(0,0,0,0.08); border-color: var(--accent); } .preview-box.drag-over { background: rgba(99, 102, 241, 0.1); border-color: var(--accent); transform: scale(1.01); } .preview-box img { max-width: 100%; max-height: 60vh; object-fit: contain; display: none; border-radius: 8px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); transition: filter 0.2s; } .upload-hint { text-align: center; color: var(--text-secondary); pointer-events: none; } .upload-icon { width: 48px; height: 48px; margin-bottom: 1rem; opacity: 0.6; color: var(--text-primary); } /* 比较提示 */ .compare-badge { position: absolute; bottom: 16px; background: rgba(0,0,0,0.7); color: white; padding: 6px 16px; border-radius: 20px; font-size: 0.85rem; backdrop-filter: blur(4px); opacity: 0; transition: opacity 0.3s; pointer-events: none; display: flex; align-items: center; gap: 6px; } .preview-box:hover .img-active + .compare-badge { opacity: 1; } /* 按钮组 */ .action-bar { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; } .BTn { border: none; padding: 0.85rem 1.5rem; border-radius: var(--radius-md); font-weight: 600; font-size: 0.95rem; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px; position: relative; overflow: hidden; } .btn:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(1); } .btn:active:not(:disabled) { transform: scale(0.96); } .btn-primary { background: var(--accent); color: white; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.3); } .btn-primary:hover:not(:disabled) { background: var(--accent-hover); box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4); } .btn-secondary { background: rgba(255,255,255,0.1); color: var(--text-primary); border: 1px solid var(--glass-border); } .btn-secondary:hover:not(:disabled) { background: rgba(255,255,255,0.2); } .btn-danger { background: rgba(239, 68, 68, 0.1); color: var(--danger); } .btn-danger:hover:not(:disabled) { background: rgba(239, 68, 68, 0.2); } /* 历史记录 */ .history-section { margin-top: 2rem; border-top: 1px solid var(--glass-border); padding-top: 1.5rem; } .history-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; font-size: 0.9rem; color: var(--text-secondary); } .history-btn { cursor: pointer; font-size: 0.8rem; text-decoration: underline; } .history-scroll { display: flex; gap: 12px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: thin; } .history-thumb { flex: 0 0 80px; height: 80px; border-radius: 12px; overflow: hidden; cursor: pointer; border: 2px solid transparent; background: rgba(0,0,0,0.1); transition: all 0.2s; } .history-thumb:hover { border-color: var(--accent); transform: translateY(-2px); } .history-thumb img { width: 100%; height: 100%; object-fit: cover; } /* 加载动画 */ .loading-overlay { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(4px); display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 10; border-radius: var(--radius-md); } .spinner { width: 40px; height: 40px; border: 3px solid rgba(255,255,255,0.3); border-top-color: #fff; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 0.8rem; } .loading-text { color: white; font-size: 0.9rem; font-weight: 500; } @keyframes spin { to { transform: rotate(360deg); } } /* Toast 提示 */ #toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; display: flex; flex-direction: column; gap: 10px; } .toast { background: rgba(30, 41, 59, 0.9); color: white; padding: 10px 20px; border-radius: 50px; font-size: 0.9rem; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 8px; opacity: 0; animation: slideIn 0.3s forwards; backdrop-filter: blur(8px); } @keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @keyframes fadeOut { to { opacity: 0; transform: translateY(-10px); } } /* 移动端适配 */ @media (max-width: 600px) { .container { padding: 1rem; } .card { padding: 1.5rem; } .preview-box { min-height: 250px; } .btn { flex: 1; justify-content: center; font-size: 0.9rem; padding: 0.7rem; } } </style> </head> <body> <div class="bg-orb orb-1"></div> <div class="bg-orb orb-2"></div> <div id="toast-container"></div> <div class="container"> <header> <h1>图片混淆</h1> <p class="subtitle">基于 Gilbert 曲线的无损像素重排技术<br>本地处理,安全隐私,支持一键还原</p> </header> <div class="card"> <div class="preview-box" id="drop-zone"> <input type="file" id="file-input" accept="image/*" style="display: none;"> <div class="upload-hint" id="upload-hint"> <svg class="upload-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg> <p style="font-weight: 600;">点击或拖拽图片到这里</p> <p style="font-size: 0.8rem; opacity: 0.7; margin-top: 4px;">支持 PNG, JPG (建议 PNG)</p> </div> <img id="display-img" alt="Preview"> <div class="compare-badge"> <svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg> 按住对比原图 </div> <div class="loading-overlay" id="loading-overlay"> <div class="spinner"></div> <div class="loading-text" id="loading-text">正在处理...</div> </div> </div> <div class="action-bar"> <button class="btn btn-primary" id="btn-enc" disabled> <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/></svg> 混淆 </button> <button class="btn btn-primary" id="btn-dec" disabled> <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"/></svg> 解混淆 </button> <button class="btn btn-secondary" id="btn-restore" disabled> <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg> 还原原图 </button> <button class="btn btn-secondary" id="btn-save" disabled> <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg> 保存结果 </button> </div> <div class="history-section"> <div class="history-header"> <span>最近记录 (本地)</span> <span class="history-btn" id="clear-history">清空</span> </div> <div class="history-scroll" id="history-list"> </div> </div> </div> </div> <script id="worker-code" type="javascript/worker"> function gilbert2d(width, height) { const coordinates = []; if (width >= height) { generate2d(0, 0, width, 0, 0, height, coordinates); } else { generate2d(0, 0, 0, height, width, 0, coordinates); } return coordinates; } function generate2d(x, y, ax, ay, bx, by, coordinates) { const w = Math.abs(ax + ay); const h = Math.abs(bx + by); const dax = Math.sign(ax), day = Math.sign(ay); const dbx = Math.sign(bx), dby = Math.sign(by); if (h === 1) { for (let i = 0; i < w; i++) { coordinates.push([x, y]); x += dax; y += day; } return; } if (w === 1) { for (let i = 0; i < h; i++) { coordinates.push([x, y]); x += dbx; y += dby; } return; } let ax2 = Math.floor(ax / 2), ay2 = Math.floor(ay / 2); let bx2 = Math.floor(bx / 2), by2 = Math.floor(by / 2); if (2 * w > 3 * h) { if ((Math.abs(ax2 + ay2) % 2) && (w > 2)) { ax2 += dax; ay2 += day; } generate2d(x, y, ax2, ay2, bx, by, coordinates); generate2d(x + ax2, y + ay2, ax - ax2, ay - ay2, bx, by, coordinates); } else { if ((Math.abs(bx2 + by2) % 2) && (h > 2)) { bx2 += dbx; by2 += dby; } generate2d(x, y, bx2, by2, ax2, ay2, coordinates); generate2d(x + bx2, y + by2, ax, ay, bx - bx2, by - by2, coordinates); generate2d(x + (ax - dax) + (bx2 - dbx), y + (ay - day) + (by2 - dby), -bx2, -by2, -(ax - ax2), -(ay - ay2), coordinates); } } self.onmessage = function(e) { try { const { type, imageData, width, height } = e.data; const curve = gilbert2d(width, height); const totalPixels = width * height; const offset = Math.floor((Math.sqrt(5) - 1) / 2 * totalPixels) % totalPixels; const newBuffer = new Uint8ClampedArray(imageData.data.length); const originalData = imageData.data; for(let i = 0; i < totalPixels; i++){ const old_pos = curve[i]; const new_pos_index = (type === 'encrypt') ? (i + offset) % totalPixels : (i - offset + totalPixels) % totalPixels; const new_pos = curve[new_pos_index]; // 实际上这里逻辑需要对应 // 重新整理逻辑以确保无误: // Encrypt: Source[Curve[i]] -> Dest[Curve[(i+offset)%N]] // Decrypt: Source[Curve[(i+offset)%N]] -> Dest[Curve[i]] // 为了性能,我们简化循环逻辑: // 我们只需知道 像素A 应该去 像素B 的位置 let srcIdx, destIdx; if (type === 'encrypt') { // 原图的 i 位置的像素(按曲线顺序),移动到 i+offset 的位置 const p1 = curve[i]; const p2 = curve[(i + offset) % totalPixels]; srcIdx = 4 * (p1[0] + p1[1] * width); destIdx = 4 * (p2[0] + p2[1] * width); } else { // 解密:当前图 i+offset 位置的像素,还原回 i 位置 const p1 = curve[(i + offset) % totalPixels]; // 混淆后的位置 const p2 = curve[i]; // 原来的位置 srcIdx = 4 * (p1[0] + p1[1] * width); // 源现在是混淆图 destIdx = 4 * (p2[0] + p2[1] * width); // 目标是原位置 } newBuffer[destIdx] = originalData[srcIdx]; newBuffer[destIdx+1] = originalData[srcIdx+1]; newBuffer[destIdx+2] = originalData[srcIdx+2]; newBuffer[destIdx+3] = originalData[srcIdx+3]; } self.postMessage({ success: true, buffer: newBuffer }, [newBuffer.buffer]); } catch (err) { self.postMessage({ success: false, error: err.message }); } }; </script> <script> // --- UI 工具: Toast 提示 --- const Toast = { show(message, type = 'info') { const container = document.getElementById('toast-container'); const el = document.createElement('div'); el.className = 'toast'; // 图标 let icon = ''; if(type === 'success') icon = '<svg width="16" height="16" fill="none" stroke="#4ade80" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"/></svg>'; else if(type === 'error') icon = '<svg width="16" height="16" fill="none" stroke="#f87171" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"/></svg>'; el.innerHTML = `${icon}<span>${message}</span>`; container.appendChild(el); setTimeout(() => { el.style.animation = 'fadeOut 0.3s forwards'; setTimeout(() => el.remove(), 300); }, 3000); } }; // --- IndexedDB 历史记录 --- const DB_CONFIG = { name: "ImgObfuscatorDB", store: "history" }; const dbapi = { async getDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_CONFIG.name, 1); req.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains(DB_CONFIG.store)) { db.createObjectStore(DB_CONFIG.store, { keyPath: "id", autoIncrement: true }); } }; req.onsuccess = e => resolve(e.target.result); req.onerror = e => reject(e); }); }, async add(blob) { const db = await this.getDB(); const tx = db.transaction(DB_CONFIG.store, "readwrite"); const store = tx.objectStore(DB_CONFIG.store); // 限制存储数量 const keys = await new Promise(res => store.getAllKeys().onsuccess = e => res(e.target.result)); if (keys.length >= 6) store.delete(keys[0]); store.add({ blob, date: Date.now() }); }, async getAll() { const db = await this.getDB(); return new Promise(resolve => { const tx = db.transaction(DB_CONFIG.store, "readonly"); tx.objectStore(DB_CONFIG.store).getAll().onsuccess = e => resolve(e.target.result); }); }, async clear() { const db = await this.getDB(); const tx = db.transaction(DB_CONFIG.store, "readwrite"); tx.objectStore(DB_CONFIG.store).clear().oncomplete = () => Toast.show("记录已清空"); } }; // --- 核心逻辑 --- const workerBlob = new Blob([document.getElementById('worker-code').textContent], { type: "text/javascript" }); const worker = new Worker(URL.createObjectURL(workerBlob)); const els = { dropZone: document.getElementById('drop-zone'), fileInput: document.getElementById('file-input'), img: document.getElementById('display-img'), hint: document.getElementById('upload-hint'), btns: { enc: document.getElementById('btn-enc'), dec: document.getElementById('btn-dec'), restore: document.getElementById('btn-restore'), save: document.getElementById('btn-save') }, overlay: document.getElementById('loading-overlay'), loadingText: document.getElementById('loading-text'), historyList: document.getElementById('history-list'), clearHistory: document.getElementById('clear-history') }; let state = { originalBlob: null, currentUrl: null, isProcessing: false }; // 初始化 (async function init() { bindEvents(); renderHistory(); })(); function bindEvents() { // 拖拽上传 els.dropZone.onclick = () => els.fileInput.click(); els.dropZone.ondragover = e => { e.preventDefault(); els.dropZone.classlist.add('drag-over'); }; els.dropZone.ondragleave = () => els.dropZone.classlist.remove('drag-over'); els.dropZone.ondrop = e => { e.preventDefault(); els.dropZone.classList.remove('drag-over'); if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]); }; els.fileInput.onchange = e => { if(e.target.files[0]) handleFile(e.target.files[0]); }; // 按钮功能 els.btns.enc.onclick = () => process('encrypt'); els.btns.dec.onclick = () => process('decrypt'); els.btns.restore.onclick = () => { if(state.originalBlob) loadImage(state.originalBlob); Toast.show("已还原至原始图片"); }; els.btns.save.onclick = download; els.clearHistory.onclick = async () => { await dbapi.clear(); renderHistory(); }; // 长按对比 const startCompare = () => { if(state.originalBlob && !state.isProcessing) els.img.src = URL.createObjectURL(state.originalBlob); }; const endCompare = () => { if(state.currentUrl && !state.isProcessing) els.img.src = state.currentUrl; }; els.img.onmousedown = startCompare; els.img.onmouseup = endCompare; els.img.onmouseleave = endCompare; els.img.ontouchstart = startCompare; els.img.ontouchend = endCompare; } async function handleFile(file) { if(!file.type.startsWith('image/')) return Toast.show("请上传图片文件", "error"); state.originalBlob = file; loadImage(file); await dbApi.add(file); renderHistory(); Toast.show("图片加载成功", "success"); } function loadImage(blob) { if(state.currentUrl) URL.revokeObjectURL(state.currentUrl); state.currentUrl = URL.createObjectURL(blob); els.img.src = state.currentUrl; els.img.style.display = 'block'; els.img.classList.add('img-active'); els.hint.style.display = 'none'; Object.values(els.btns).forEach(btn => btn.disabled = false); } function process(type) { if(state.isProcessing) return; setLoading(true, type === 'encrypt' ? '正在混淆像素...' : '正在解密像素...'); const img = new Image(); img.src = state.currentUrl; img.onload = () => { const cvs = document.createElement('canvas'); cvs.width = img.naturalWidth; cvs.height = img.naturalHeight; const ctx = cvs.getContext('2d'); ctx.drawImage(img, 0, 0); worker.postMessage({ type, imageData: ctx.getImageData(0, 0, cvs.width, cvs.height), width: cvs.width, height: cvs.height }); }; } worker.onmessage = e => { const { success, buffer, error } = e.data; if(success) { const cvs = document.createElement('canvas'); cvs.width = els.img.naturalWidth; cvs.height = els.img.naturalHeight; const ctx = cvs.getContext('2d'); ctx.putImageData(new ImageData(buffer, cvs.width, cvs.height), 0, 0); cvs.toBlob(blob => { loadImage(blob); setLoading(false); Toast.show("处理完成", "success"); }, 'image/png'); } else { setLoading(false); Toast.show("处理失败: " + error, "error"); } }; function setLoading(isLoading, text) { state.isProcessing = isLoading; els.overlay.style.display = isLoading ? 'flex' : 'none'; els.loadingText.textContent = text; Object.values(els.btns).forEach(btn => btn.disabled = isLoading); } function download() { if(!state.currentUrl) return; const a = document.createElement('a'); a.href = state.currentUrl; a.download = `obfuscated_${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); Toast.show("已开始下载 (PNG格式)", "success"); } async function renderHistory() { const list = await dbApi.getAll(); els.historyList.innerHTML = ''; if(list.length === 0) { els.historyList.innerHTML = '<div style="color:var(--text-secondary);font-size:0.8rem;padding:0 10px;">暂无记录</div>'; return; } [...list].reverse().forEach(item => { const div = document.createElement('div'); div.className = 'history-thumb'; const img = document.createElement('img'); img.src = URL.createObjectURL(item.blob); div.appendChild(img); div.onclick = () => { state.originalBlob = item.blob; loadImage(item.blob); Toast.show("已加载历史图片"); }; els.historyList.appendChild(div); }); } </script> </body> </html>
代码可在任何浏览器上稳定运行、具备现代交互体验的专业级图像处理工具,满足用户对功能、性能与隐私安全的多重需求。欢迎大家交流互鉴






