HTML图片混淆与解混淆 无损解析 本地网页应用

HTML图片混淆与解混淆 无损解析 本地网页应用

一、核心技术与性能优化

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 引用,确保还原的是未经处理的原始图片;“保存结果” 功能在保存文件时,会自动为文件名添加时间戳,避免文件覆盖问题。两个功能均无需寻找隐藏入口,操作更直接高效。

效果预览

图片[1]-HTML图片混淆与解混淆 无损解析 本地网页应用-QQ沐编程

源代码

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图片混淆 - 专业版</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
    <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>

这个代码可在任何浏览器上稳定运行、具备现代交互体验的专业级图像处理工具,满足用户对功能、性能与隐私安全的多重需求。欢迎大家交流互鉴

© 版权声明
THE END
喜欢就支持一下吧
点赞15赞赏 分享