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

近期无聊在抖音部分视频的评论区看到了混淆图,突发奇想写一个可以混淆和解混淆的前端,可以用来轻加密图片。undefined并且网上大多数解混淆生成的图片质量不是100%,这个本地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 引用,确保还原的是未经处理的原始图片;“保存结果” 功能在保存文件时,会自动为文件名添加时间戳,避免文件覆盖问题。两个功能均无需寻找隐藏入口,操作更直接高效。

以下是html源码

  1. <!DOCTYPE html>
  2. <html lang=“zh-cn”>
  3. <head>
  4.     <meta charset=“UTF-8”>
  5.     <meta name=“viewport” content=“width=device-width, initial-scale=1.0”>
  6.     <title>图片混淆 – 专业版</title>
  7.     <link href=“https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap” rel=“stylesheet”>
  8.     <style>
  9.         :root {
  10.             –bg-gradient-1: #4f46e5;
  11.             –bg-gradient-2: #ec4899;
  12.             –glass-bg: rgba(255, 255, 255, 0.7);
  13.             –glass-border: rgba(255, 255, 255, 0.5);
  14.             –text-primary: #1e293b;
  15.             –text-secondary: #64748b;
  16.             –accent: #4f46e5;
  17.             –accent-hover: #4338ca;
  18.             –danger: #ef4444;
  19.             –radius-lg: 24px;
  20.             –radius-md: 12px;
  21.             –shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
  22.         }
  23.         /* 深色模式适配 */
  24.         @media (prefers-color-scheme: dark) {
  25.             :root {
  26.                 –glass-bg: rgba(30, 41, 59, 0.7);
  27.                 –glass-border: rgba(255, 255, 255, 0.1);
  28.                 –text-primary: #f8fafc;
  29.                 –text-secondary: #94a3b8;
  30.                 –accent: #6366f1;
  31.                 –accent-hover: #818cf8;
  32.             }
  33.         }
  34.         * { margin: 0; padding: 0; box-sizing: border-box; outline: none; }
  35.         body {
  36.             font-family: ‘Inter’, -apple-system, BlinkMacSystemFont, sans-serif;
  37.             min-height: 100vh;
  38.             display: flex;
  39.             flex-direction: column;
  40.             align-items: center;
  41.             background: #0f172a;
  42.             color: var(–text-primary);
  43.             overflow-x: hidden;
  44.             position: relative;
  45.         }
  46.         /* 动态流光背景 */
  47.         .bg-orb {
  48.             position: fixed;
  49.             border-radius: 50%;
  50.             filter: blur(80px);
  51.             z-index: -1;
  52.             animation: float 10s infinite ease-in-out;
  53.             opacity: 0.6;
  54.         }
  55.         .orb-1 { width: 400px; height: 400px; background: var(–bg-gradient-1); top: -100px; left: -100px; }
  56.         .orb-2 { width: 300px; height: 300px; background: var(–bg-gradient-2); bottom: -50px; right: -50px; animation-delay: -5s; }
  57.         @keyframes float {
  58.             0%, 100% { transform: translate(0, 0); }
  59.             50% { transform: translate(30px, 50px); }
  60.         }
  61.         /* 主容器 */
  62.         .container {
  63.             width: 100%;
  64.             max-width: 900px;
  65.             padding: 2rem 1.5rem;
  66.             z-index: 1;
  67.         }
  68.         header {
  69.             text-align: center;
  70.             margin-bottom: 2.5rem;
  71.         }
  72.         h1 {
  73.             font-size: 2.2rem;
  74.             font-weight: 700;
  75.             margin-bottom: 0.5rem;
  76.             letter-spacing: -0.025em;
  77.         }
  78.         .subtitle {
  79.             color: var(–text-secondary);
  80.             font-size: 0.95rem;
  81.             max-width: 500px;
  82.             margin: 0 auto;
  83.             line-height: 1.5;
  84.         }
  85.         /* 卡片风格 */
  86.         .card {
  87.             background: var(–glass-bg);
  88.             backdrop-filter: blur(20px);
  89.             -webkit-backdrop-filter: blur(20px);
  90.             border: 1px solid var(–glass-border);
  91.             border-radius: var(–radius-lg);
  92.             padding: 2rem;
  93.             box-shadow: var(–shadow);
  94.             transition: transform 0.3s ease;
  95.         }
  96.         /* 图片预览区 */
  97.         .preview-box {
  98.             width: 100%;
  99.             min-height: 350px;
  100.             border: 2px dashed var(–glass-border);
  101.             border-radius: var(–radius-md);
  102.             background: rgba(0,0,0,0.05);
  103.             display: flex;
  104.             justify-content: center;
  105.             align-items: center;
  106.             position: relative;
  107.             overflow: hidden;
  108.             cursor: pointer;
  109.             transition: all 0.3s ease;
  110.             margin-bottom: 2rem;
  111.         }
  112.         .preview-box:hover { background: rgba(0,0,0,0.08); border-color: var(–accent); }
  113.         .preview-box.drag-over { background: rgba(99, 102, 241, 0.1); border-color: var(–accent); transform: scale(1.01); }
  114.         .preview-box img {
  115.             max-width: 100%;
  116.             max-height: 60vh;
  117.             object-fit: contain;
  118.             display: none;
  119.             border-radius: 8px;
  120.             box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
  121.             transition: filter 0.2s;
  122.         }
  123.         .upload-hint {
  124.             text-align: center;
  125.             color: var(–text-secondary);
  126.             pointer-events: none;
  127.         }
  128.         .upload-icon { width: 48px; height: 48px; margin-bottom: 1rem; opacity: 0.6; color: var(–text-primary); }
  129.         /* 比较提示 */
  130.         .compare-badge {
  131.             position: absolute;
  132.             bottom: 16px;
  133.             background: rgba(0,0,0,0.7);
  134.             color: white;
  135.             padding: 6px 16px;
  136.             border-radius: 20px;
  137.             font-size: 0.85rem;
  138.             backdrop-filter: blur(4px);
  139.             opacity: 0;
  140.             transition: opacity 0.3s;
  141.             pointer-events: none;
  142.             display: flex;
  143.             align-items: center;
  144.             gap: 6px;
  145.         }
  146.         .preview-box:hover .img-active + .compare-badge { opacity: 1; }
  147.         /* 按钮组 */
  148.         .action-bar {
  149.             display: flex;
  150.             gap: 1rem;
  151.             flex-wrap: wrap;
  152.             justify-content: center;
  153.         }
  154.         .btn {
  155.             border: none;
  156.             padding: 0.85rem 1.5rem;
  157.             border-radius: var(–radius-md);
  158.             font-weight: 600;
  159.             font-size: 0.95rem;
  160.             cursor: pointer;
  161.             transition: all 0.2s;
  162.             display: flex;
  163.             align-items: center;
  164.             gap: 8px;
  165.             position: relative;
  166.             overflow: hidden;
  167.         }
  168.         .btn:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(1); }
  169.         .btn:active:not(:disabled) { transform: scale(0.96); }
  170.         .btn-primary { background: var(–accent); color: white; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.3); }
  171.         .btn-primary:hover:not(:disabled) { background: var(–accent-hover); box-shadow: 0 10px 15px -3px rgba(79, 70, 229, 0.4); }
  172.         .btn-secondary { background: rgba(255,255,255,0.1); color: var(–text-primary); border: 1px solid var(–glass-border); }
  173.         .btn-secondary:hover:not(:disabled) { background: rgba(255,255,255,0.2); }
  174.         .btn-danger { background: rgba(239, 68, 68, 0.1); color: var(–danger); }
  175.         .btn-danger:hover:not(:disabled) { background: rgba(239, 68, 68, 0.2); }
  176.         /* 历史记录 */
  177.         .history-section {
  178.             margin-top: 2rem;
  179.             border-top: 1px solid var(–glass-border);
  180.             padding-top: 1.5rem;
  181.         }
  182.          
  183.         .history-header {
  184.             display: flex;
  185.             justify-content: space-between;
  186.             align-items: center;
  187.             margin-bottom: 1rem;
  188.             font-size: 0.9rem;
  189.             color: var(–text-secondary);
  190.         }
  191.          
  192.         .history-btn { cursor: pointer; font-size: 0.8rem; text-decoration: underline; }
  193.          
  194.         .history-scroll {
  195.             display: flex;
  196.             gap: 12px;
  197.             overflow-x: auto;
  198.             padding-bottom: 8px;
  199.             scrollbar-width: thin;
  200.         }
  201.          
  202.         .history-thumb {
  203.             flex: 0 0 80px;
  204.             height: 80px;
  205.             border-radius: 12px;
  206.             overflow: hidden;
  207.             cursor: pointer;
  208.             border: 2px solid transparent;
  209.             background: rgba(0,0,0,0.1);
  210.             transition: all 0.2s;
  211.         }
  212.         .history-thumb:hover { border-color: var(–accent); transform: translateY(-2px); }
  213.         .history-thumb img { width: 100%; height: 100%; object-fit: cover; }
  214.         /* 加载动画 */
  215.         .loading-overlay {
  216.             position: absolute;
  217.             inset: 0;
  218.             background: rgba(15, 23, 42, 0.6);
  219.             backdrop-filter: blur(4px);
  220.             display: none;
  221.             flex-direction: column;
  222.             align-items: center;
  223.             justify-content: center;
  224.             z-index: 10;
  225.             border-radius: var(–radius-md);
  226.         }
  227.         .spinner {
  228.             width: 40px; height: 40px;
  229.             border: 3px solid rgba(255,255,255,0.3);
  230.             border-top-color: #fff;
  231.             border-radius: 50%;
  232.             animation: spin 1s linear infinite;
  233.             margin-bottom: 0.8rem;
  234.         }
  235.         .loading-text { color: white; font-size: 0.9rem; font-weight: 500; }
  236.         @keyframes spin { to { transform: rotate(360deg); } }
  237.         /* Toast 提示 */
  238.         #toast-container {
  239.             position: fixed;
  240.             top: 20px;
  241.             left: 50%;
  242.             transform: translateX(-50%);
  243.             z-index: 1000;
  244.             display: flex;
  245.             flex-direction: column;
  246.             gap: 10px;
  247.         }
  248.         .toast {
  249.             background: rgba(30, 41, 59, 0.9);
  250.             color: white;
  251.             padding: 10px 20px;
  252.             border-radius: 50px;
  253.             font-size: 0.9rem;
  254.             box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  255.             display: flex;
  256.             align-items: center;
  257.             gap: 8px;
  258.             opacity: 0;
  259.             animation: slideIn 0.3s forwards;
  260.             backdrop-filter: blur(8px);
  261.         }
  262.         @keyframes slideIn {
  263.             from { transform: translateY(-20px); opacity: 0; }
  264.             to { transform: translateY(0); opacity: 1; }
  265.         }
  266.         @keyframes fadeOut {
  267.             to { opacity: 0; transform: translateY(-10px); }
  268.         }
  269.         /* 移动端适配 */
  270.         @media (max-width: 600px) {
  271.             .container { padding: 1rem; }
  272.             .card { padding: 1.5rem; }
  273.             .preview-box { min-height: 250px; }
  274.             .btn { flex: 1; justify-content: center; font-size: 0.9rem; padding: 0.7rem; }
  275.         }
  276.     </style>
  277. </head>
  278. <body>
  279.     <div class=“bg-orb orb-1”></div>
  280.     <div class=“bg-orb orb-2”></div>
  281.     <div id=“toast-container”></div>
  282.     <div class=“container”>
  283.         <header>
  284.             <h1>图片混淆</h1>
  285.             <p class=“subtitle”>基于 Gilbert 曲线的无损像素重排技术<br>本地处理,安全隐私,支持一键还原</p>
  286.         </header>
  287.         <div class=“card”>
  288.             <div class=“preview-box” id=“drop-zone”>
  289.                 <input type=“file” id=“file-input” accept=“image/*” style=“display: none;”>
  290.                 <div class=“upload-hint” id=“upload-hint”>
  291.                     <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>
  292.                     <p style=“font-weight: 600;”>点击或拖拽图片到这里</p>
  293.                     <p style=“font-size: 0.8rem; opacity: 0.7; margin-top: 4px;”>支持 PNG, JPG (建议 PNG)</p>
  294.                 </div>
  295.                 <img id=“display-img” alt=“Preview”>
  296.                 <div class=“compare-badge”>
  297.                     <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>
  298.                     按住对比原图
  299.                 </div>
  300.                 <div class=“loading-overlay” id=“loading-overlay”>
  301.                     <div class=“spinner”></div>
  302.                     <div class=“loading-text” id=“loading-text”>正在处理…</div>
  303.                 </div>
  304.             </div>
  305.             <div class=“action-bar”>
  306.                 <button class=“btn btn-primary” id=“btn-enc” disabled>
  307.                     <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>
  308.                     混淆
  309.                 </button>
  310.                 <button class=“btn btn-primary” id=“btn-dec” disabled>
  311.                     <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>
  312.                     解混淆
  313.                 </button>
  314.                 <button class=“btn btn-secondary” id=“btn-restore” disabled>
  315.                     <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>
  316.                     还原原图
  317.                 </button>
  318.                 <button class=“btn btn-secondary” id=“btn-save” disabled>
  319.                     <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>
  320.                     保存结果
  321.                 </button>
  322.             </div>
  323.             <div class=“history-section”>
  324.                 <div class=“history-header”>
  325.                     <span>最近记录 (本地)</span>
  326.                     <span class=“history-btn” id=“clear-history”>清空</span>
  327.                 </div>
  328.                 <div class=“history-scroll” id=“history-list”>
  329.                     </div>
  330.             </div>
  331.         </div>
  332.     </div>
  333.     <script id=“worker-code” type=“javascript/worker”>
  334.         function gilbert2d(width, height) {
  335.             const coordinates = [];
  336.             if (width >= height) {
  337.                 generate2d(0, 0, width, 0, 0, height, coordinates);
  338.             } else {
  339.                 generate2d(0, 0, 0, height, width, 0, coordinates);
  340.             }
  341.             return coordinates;
  342.         }
  343.         function generate2d(x, y, ax, ay, bx, by, coordinates) {
  344.             const w = Math.abs(ax + ay);
  345.             const h = Math.abs(bx + by);
  346.             const dax = Math.sign(ax), day = Math.sign(ay);
  347.             const dbx = Math.sign(bx), dby = Math.sign(by);
  348.             if (h === 1) {
  349.                 for (let i = 0; i < w; i++) {
  350.                     coordinates.push([x, y]); x += dax; y += day;
  351.                 }
  352.                 return;
  353.             }
  354.             if (w === 1) {
  355.                 for (let i = 0; i < h; i++) {
  356.                     coordinates.push([x, y]); x += dbx; y += dby;
  357.                 }
  358.                 return;
  359.             }
  360.             let ax2 = Math.floor(ax / 2), ay2 = Math.floor(ay / 2);
  361.             let bx2 = Math.floor(bx / 2), by2 = Math.floor(by / 2);
  362.             if (2 * w > 3 * h) {
  363.                 if ((Math.abs(ax2 + ay2) % 2) && (w > 2)) { ax2 += dax; ay2 += day; }
  364.                 generate2d(x, y, ax2, ay2, bx, by, coordinates);
  365.                 generate2d(x + ax2, y + ay2, ax – ax2, ay – ay2, bx, by, coordinates);
  366.             } else {
  367.                 if ((Math.abs(bx2 + by2) % 2) && (h > 2)) { bx2 += dbx; by2 += dby; }
  368.                 generate2d(x, y, bx2, by2, ax2, ay2, coordinates);
  369.                 generate2d(x + bx2, y + by2, ax, ay, bx – bx2, by – by2, coordinates);
  370.                 generate2d(x + (ax – dax) + (bx2 – dbx), y + (ay – day) + (by2 – dby),
  371.                     -bx2, -by2, -(ax – ax2), -(ay – ay2), coordinates);
  372.             }
  373.         }
  374.         self.onmessage = function(e) {
  375.             try {
  376.                 const { type, imageData, width, height } = e.data;
  377.                 const curve = gilbert2d(width, height);
  378.                 const totalPixels = width * height;
  379.                 const offset = Math.floor((Math.sqrt(5) – 1) / 2 * totalPixels) % totalPixels;
  380.                  
  381.                 const newBuffer = new Uint8ClampedArray(imageData.data.length);
  382.                 const originalData = imageData.data;
  383.                 for(let i = 0; i < totalPixels; i++){
  384.                     const old_pos = curve[i];
  385.                     const new_pos_index = (type === ‘encrypt’)
  386.                         ? (i + offset) % totalPixels
  387.                         : (i – offset + totalPixels) % totalPixels;
  388.                     const new_pos = curve[new_pos_index]; // 实际上这里逻辑需要对应
  389.                     // 重新整理逻辑以确保无误:
  390.                     // Encrypt: Source[Curve[i]] -> Dest[Curve[(i+offset)%N]]
  391.                     // Decrypt: Source[Curve[(i+offset)%N]] -> Dest[Curve[i]]
  392.                      
  393.                     // 为了性能,我们简化循环逻辑:
  394.                     // 我们只需知道 像素A 应该去 像素B 的位置
  395.                      
  396.                     let srcIdx, destIdx;
  397.                      
  398.                     if (type === ‘encrypt’) {
  399.                         // 原图的 i 位置的像素(按曲线顺序),移动到 i+offset 的位置
  400.                         const p1 = curve[i];
  401.                         const p2 = curve[(i + offset) % totalPixels];
  402.                         srcIdx = 4 * (p1[0] + p1[1] * width);
  403.                         destIdx = 4 * (p2[0] + p2[1] * width);
  404.                     } else {
  405.                         // 解密:当前图 i+offset 位置的像素,还原回 i 位置
  406.                         const p1 = curve[(i + offset) % totalPixels]; // 混淆后的位置
  407.                         const p2 = curve[i]; // 原来的位置
  408.                         srcIdx = 4 * (p1[0] + p1[1] * width); // 源现在是混淆图
  409.                         destIdx = 4 * (p2[0] + p2[1] * width); // 目标是原位置
  410.                     }
  411.                     newBuffer[destIdx] = originalData[srcIdx];
  412.                     newBuffer[destIdx+1] = originalData[srcIdx+1];
  413.                     newBuffer[destIdx+2] = originalData[srcIdx+2];
  414.                     newBuffer[destIdx+3] = originalData[srcIdx+3];
  415.                 }
  416.                 self.postMessage({ success: true, buffer: newBuffer }, [newBuffer.buffer]);
  417.             } catch (err) {
  418.                 self.postMessage({ success: false, error: err.message });
  419.             }
  420.         };
  421.     </script>
  422.     <script>
  423.         // — UI 工具: Toast 提示 —
  424.         const Toast = {
  425.             show(message, type = ‘info’) {
  426.                 const container = document.getElementById(‘toast-container’);
  427.                 const el = document.createElement(‘div’);
  428.                 el.className = ‘toast’;
  429.                 // 图标
  430.                 let icon = ;
  431.                 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>’;
  432.                 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>’;
  433.                  
  434.                 el.innerHTML = `${icon}<span>${message}</span>`;
  435.                 container.appendChild(el);
  436.                  
  437.                 setTimeout(() => {
  438.                     el.style.animation = ‘fadeOut 0.3s forwards’;
  439.                     setTimeout(() => el.remove(), 300);
  440.                 }, 3000);
  441.             }
  442.         };
  443.         // — IndexedDB 历史记录 —
  444.         const DB_CONFIG = { name: “ImgObfuscatorDB”, store: “history” };
  445.         const dbApi = {
  446.             async getDB() {
  447.                 return new Promise((resolve, reject) => {
  448.                     const req = indexedDB.open(DB_CONFIG.name, 1);
  449.                     req.onupgradeneeded = e => {
  450.                         const db = e.target.result;
  451.                         if (!db.objectStoreNames.contains(DB_CONFIG.store)) {
  452.                             db.createObjectStore(DB_CONFIG.store, { keyPath: “id”, autoIncrement: true });
  453.                         }
  454.                     };
  455.                     req.onsuccess = e => resolve(e.target.result);
  456.                     req.onerror = e => reject(e);
  457.                 });
  458.             },
  459.             async add(blob) {
  460.                 const db = await this.getDB();
  461.                 const tx = db.transaction(DB_CONFIG.store, “readwrite”);
  462.                 const store = tx.objectStore(DB_CONFIG.store);
  463.                  
  464.                 // 限制存储数量
  465.                 const keys = await new Promise(res => store.getAllKeys().onsuccess = e => res(e.target.result));
  466.                 if (keys.length >= 6) store.delete(keys[0]);
  467.                  
  468.                 store.add({ blob, date: Date.now() });
  469.             },
  470.             async getAll() {
  471.                 const db = await this.getDB();
  472.                 return new Promise(resolve => {
  473.                     const tx = db.transaction(DB_CONFIG.store, “readonly”);
  474.                     tx.objectStore(DB_CONFIG.store).getAll().onsuccess = e => resolve(e.target.result);
  475.                 });
  476.             },
  477.             async clear() {
  478.                 const db = await this.getDB();
  479.                 const tx = db.transaction(DB_CONFIG.store, “readwrite”);
  480.                 tx.objectStore(DB_CONFIG.store).clear().oncomplete = () => Toast.show(“记录已清空”);
  481.             }
  482.         };
  483.         // — 核心逻辑 —
  484.         const workerBlob = new Blob([document.getElementById(‘worker-code’).textContent], { type: “text/javascript” });
  485.         const worker = new Worker(URL.createObjectURL(workerBlob));
  486.         const els = {
  487.             dropZone: document.getElementById(‘drop-zone’),
  488.             fileInput: document.getElementById(‘file-input’),
  489.             img: document.getElementById(‘display-img’),
  490.             hint: document.getElementById(‘upload-hint’),
  491.             btns: {
  492.                 enc: document.getElementById(‘btn-enc’),
  493.                 dec: document.getElementById(‘btn-dec’),
  494.                 restore: document.getElementById(‘btn-restore’),
  495.                 save: document.getElementById(‘btn-save’)
  496.             },
  497.             overlay: document.getElementById(‘loading-overlay’),
  498.             loadingText: document.getElementById(‘loading-text’),
  499.             historyList: document.getElementById(‘history-list’),
  500.             clearHistory: document.getElementById(‘clear-history’)
  501.         };
  502.         let state = {
  503.             originalBlob: null,
  504.             currentUrl: null,
  505.             isProcessing: false
  506.         };
  507.         // 初始化
  508.         (async function init() {
  509.             bindEvents();
  510.             renderHistory();
  511.         })();
  512.         function bindEvents() {
  513.             // 拖拽上传
  514.             els.dropZone.onclick = () => els.fileInput.click();
  515.             els.dropZone.ondragover = e => { e.preventDefault(); els.dropZone.classList.add(‘drag-over’); };
  516.             els.dropZone.ondragleave = () => els.dropZone.classList.remove(‘drag-over’);
  517.             els.dropZone.ondrop = e => {
  518.                 e.preventDefault();
  519.                 els.dropZone.classList.remove(‘drag-over’);
  520.                 if(e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
  521.             };
  522.             els.fileInput.onchange = e => { if(e.target.files[0]) handleFile(e.target.files[0]); };
  523.             // 按钮功能
  524.             els.btns.enc.onclick = () => process(‘encrypt’);
  525.             els.btns.dec.onclick = () => process(‘decrypt’);
  526.             els.btns.restore.onclick = () => {
  527.                 if(state.originalBlob) loadImage(state.originalBlob);
  528.                 Toast.show(“已还原至原始图片”);
  529.             };
  530.             els.btns.save.onclick = download;
  531.             els.clearHistory.onclick = async () => { await dbApi.clear(); renderHistory(); };
  532.             // 长按对比
  533.             const startCompare = () => { if(state.originalBlob && !state.isProcessing) els.img.src = URL.createObjectURL(state.originalBlob); };
  534.             const endCompare = () => { if(state.currentUrl && !state.isProcessing) els.img.src = state.currentUrl; };
  535.             
  536.             els.img.onmousedown = startCompare;
  537.             els.img.onmouseup = endCompare;
  538.             els.img.onmouseleave = endCompare;
  539.             els.img.ontouchstart = startCompare;
  540.             els.img.ontouchend = endCompare;
  541.         }
  542.         async function handleFile(file) {
  543.             if(!file.type.startsWith(‘image/’)) return Toast.show(“请上传图片文件”, “error”);
  544.             
  545.             state.originalBlob = file;
  546.             loadImage(file);
  547.             await dbApi.add(file);
  548.             renderHistory();
  549.             Toast.show(“图片加载成功”, “success”);
  550.         }
  551.         function loadImage(blob) {
  552.             if(state.currentUrl) URL.revokeObjectURL(state.currentUrl);
  553.             state.currentUrl = URL.createObjectURL(blob);
  554.             els.img.src = state.currentUrl;
  555.             els.img.style.display = ‘block’;
  556.             els.img.classList.add(‘img-active’);
  557.             els.hint.style.display = ‘none’;
  558.             
  559.             Object.values(els.btns).forEach(btn => btn.disabled = false);
  560.         }
  561.         function process(type) {
  562.             if(state.isProcessing) return;
  563.             setLoading(true, type === ‘encrypt’ ? ‘正在混淆像素…’ : ‘正在解密像素…’);
  564.             const img = new Image();
  565.             img.src = state.currentUrl;
  566.             img.onload = () => {
  567.                 const cvs = document.createElement(‘canvas’);
  568.                 cvs.width = img.naturalWidth;
  569.                 cvs.height = img.naturalHeight;
  570.                 const ctx = cvs.getContext(‘2d’);
  571.                 ctx.drawImage(img, 0, 0);
  572.                  
  573.                 worker.postMessage({
  574.                     type,
  575.                     imageData: ctx.getImageData(0, 0, cvs.width, cvs.height),
  576.                     width: cvs.width,
  577.                     height: cvs.height
  578.                 });
  579.             };
  580.         }
  581.         worker.onmessage = e => {
  582.             const { success, buffer, error } = e.data;
  583.             if(success) {
  584.                 const cvs = document.createElement(‘canvas’);
  585.                 cvs.width = els.img.naturalWidth;
  586.                 cvs.height = els.img.naturalHeight;
  587.                 const ctx = cvs.getContext(‘2d’);
  588.                 ctx.putImageData(new ImageData(buffer, cvs.width, cvs.height), 0, 0);
  589.                  
  590.                 cvs.toBlob(blob => {
  591.                     loadImage(blob);
  592.                     setLoading(false);
  593.                     Toast.show(“处理完成”, “success”);
  594.                 }, ‘image/png’);
  595.             } else {
  596.                 setLoading(false);
  597.                 Toast.show(“处理失败: “ + error, “error”);
  598.             }
  599.         };
  600.         function setLoading(isLoading, text) {
  601.             state.isProcessing = isLoading;
  602.             els.overlay.style.display = isLoading ? ‘flex’ : ‘none’;
  603.             els.loadingText.textContent = text;
  604.             Object.values(els.btns).forEach(btn => btn.disabled = isLoading);
  605.         }
  606.         function download() {
  607.             if(!state.currentUrl) return;
  608.             const a = document.createElement(‘a’);
  609.             a.href = state.currentUrl;
  610.             a.download = `obfuscated_${Date.now()}.png`;
  611.             document.body.appendChild(a);
  612.             a.click();
  613.             document.body.removeChild(a);
  614.             Toast.show(“已开始下载 (PNG格式)”, “success”);
  615.         }
  616.         async function renderHistory() {
  617.             const list = await dbApi.getAll();
  618.             els.historyList.innerHTML = ;
  619.             
  620.             if(list.length === 0) {
  621.                 els.historyList.innerHTML = ‘<div style=”color:var(–text-secondary);font-size:0.8rem;padding:0 10px;”>暂无记录</div>’;
  622.                 return;
  623.             }
  624.             […list].reverse().forEach(item => {
  625.                 const div = document.createElement(‘div’);
  626.                 div.className = ‘history-thumb’;
  627.                 const img = document.createElement(‘img’);
  628.                 img.src = URL.createObjectURL(item.blob);
  629.                 div.appendChild(img);
  630.                 div.onclick = () => {
  631.                     state.originalBlob = item.blob;
  632.                     loadImage(item.blob);
  633.                     Toast.show(“已加载历史图片”);
  634.                 };
  635.                 els.historyList.appendChild(div);
  636.             });
  637.         }
  638.     </script>
  639. </body>
  640. </html>
复制代码

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

【版权声明】:服务器导航网所有内容均来自网络和部分原创,若无意侵犯到您的权利,请及时与联系 QQ 2232175042,将在48小时内删除相关内容!!

给TA服务器
共{{data.count}}人
人已服务器
LINUX技术教程

宝塔:三重回馈上线(免费1年SSL)

2025-12-3 5:02:30

技术教程

Comfyui教学AI修图换脸视频一看就会

2025-12-4 5:50:20

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索