一、核心技术与性能优化
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沐编程](https://www.qqmu.com/wp-content/uploads/2025/12/tupianhunxiao.jpg)
源代码
<!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>
这个代码可在任何浏览器上稳定运行、具备现代交互体验的专业级图像处理工具,满足用户对功能、性能与隐私安全的多重需求。欢迎大家交流互鉴












