近期无聊在抖音部分视频的评论区看到了混淆图,突发奇想写一个可以混淆和解混淆的前端,可以用来轻加密图片。undefined并且网上大多数解混淆生成的图片质量不是100%,这个本地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 引用,确保还原的是未经处理的原始图片;“保存结果” 功能在保存文件时,会自动为文件名添加时间戳,避免文件覆盖问题。两个功能均无需寻找隐藏入口,操作更直接高效。
以下是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>
复制代码
这个代码可在任何浏览器上稳定运行、具备现代交互体验的专业级图像处理工具,满足用户对功能、性能与隐私安全的多重需求。欢迎大家交流互鉴
