即梦AI生图去水印实用指南:免费方法,轻松拿下纯净原图

📅 2026/7/29 14:00:33
即梦AI生图去水印实用指南:免费方法,轻松拿下纯净原图
说明本文以浏览器插件的技术实现为例仅用于下载本人创作、已获授权或平台明确允许保存的内容。请遵守网站服务条款、版权规则与当地法律不要绕过 DRM、付费墙或访问控制。很多内容平台的作品页面上图片和视频都被组件、按钮、遮罩层包得严严实实。右键保存不一定有效复制到的也可能只是缩略图。但浏览器要把内容显示出来就必须先拿到某种媒体资源可能是img的真实地址也可能是video、source或者播放器使用的分片流。于是我做了一个很轻量的 Chrome Manifest V3 插件鼠标移到图片或视频附近时页面上出现一个下载按钮点击后由插件后台调用浏览器下载能力保存文件。它不依赖框架不需要扫描整页也不修改原网页布局。真正值得讲的不是“加一个下载按钮”而是背后的三个工程问题怎样找到浏览器实际加载的媒体地址怎样在不破坏页面的前提下放入交互入口怎样把网页里的点击动作安全地交给扩展后台执行下面拆开来说。一、先纠正一个误区无水印资源并不总是存在有些页面的水印只是 CSS、DOM 或 Canvas 叠加层底层img加载的文件本身没有水印。此时直接取得媒体 URL确实可能保存到底层文件。但这不是通用规律。平台也可能在服务端把水印直接写入图片或视频只向页面下发压缩图或带水印版本使用临时签名 URL并校验 Cookie、Referer 或有效期通过 MSE、HLS、DASH 等方式播放分片视频对内容施加 DRM 或其他访问控制。所以更准确的表述是插件下载的是浏览器当前可访问的媒体资源不保证一定是原图、最高画质或无水印版本。对普通img和直链video这个思路通常很有效遇到分片、鉴权和 DRM则需要止步于合规边界。二、为什么要拆成 content script 和 backgroundManifest V3 扩展里这两个角色分工明确网页 DOM │ │ content.js识别图片/视频、显示按钮、提取 URL │ └── chrome.runtime.sendMessage() │ ▼ background.jsService Worker 调用 chrome.downloads.download()content.js能读取页面 DOM却不能直接使用所有扩展专属 APIbackground.js拿得到chrome.downloads但不能操作网页 DOM。因此最自然的设计就是内容脚本负责“看见什么”后台脚本负责“下载什么”两者用消息传递连接。manifest.json的核心配置如下{ manifest_version: 3, permissions: [downloads], host_permissions: [all_urls], content_scripts: [ { matches: [all_urls], js: [content.js], css: [content.css], run_at: document_idle } ], background: { service_worker: background.js } }如果插件只服务于少数网站建议把all_urls收窄到明确域名。权限越少用户越容易理解和信任。三、图片 URL不要只盯着src现代网页中的图片经常使用响应式加载和懒加载。一个img上可能同时存在currentSrc浏览器最终选中的资源src默认地址srcset多个尺寸候选data-src/data-srcset懒加载库保存的真实地址。因此提取顺序应该优先尊重浏览器已经做出的选择function getImageUrl(img) { let url img.currentSrc || img.src || img.dataset.src || ; if (!url) { const srcset img.getAttribute(srcset) || img.getAttribute(data-srcset); url pickBestFromSrcset(srcset); } if (!url || url.startsWith(data:) || url.startsWith(blob:)) { return null; } return new URL(url, location.href).href; }这里有两个细节很重要。第一currentSrc通常比src更可靠因为它代表浏览器根据屏幕密度、视口宽度和srcset规则真正选中的资源。第二如果只能解析srcset不要机械地取第一个候选。srcset往往从小图排到大图更稳妥的做法是解析每个候选的w或x描述符再选择规格最高的一个。四、别把扩展名当成唯一真相CDN 地址经常没有标准文件后缀例如https://cdn.example.com/asset/abc123?formatwebp https://cdn.example.com/image.webp/resize/1080插件可以按以下顺序做“类型猜测”检查 URL 路径末尾的扩展名检查路径中的格式片段检查format、type、ext等查询参数必要时在下载后参考响应的Content-Type。但要注意URL 规则只能帮助识别不能证明文件内容一定与后缀一致。生产级实现最好把“识别媒体”和“生成文件名”分开避免因为猜错扩展名而保存出无法打开的文件。五、视频为什么比图片难得多普通直链视频很简单function getVideoUrl(video) { let url video.currentSrc || video.src || ; if (!url || url.startsWith(blob:)) { const source video.querySelector(source[src]); url source?.src || ; } if (!url || url.startsWith(blob:) || url.startsWith(data:)) { return null; } return new URL(url, location.href).href; }真正棘手的是blob:。当播放器使用 MSEMedia Source Extensions时页面会把一段段媒体数据送进SourceBuffer。此时blob:https://example.com/xxxxxxxx只是浏览器内部对象的临时地址并不是一个可直接下载的远程文件。如果source中仍有普通 URL可以继续使用如果只有 HLS、DASH 或其他分片清单就进入了完全不同的处理范畴。插件不应该把“下载 blob 地址”伪装成成功更不应尝试绕过 DRM。六、一个按钮如何做到不破坏原页面我选择的方案是全页只创建一个按钮把它追加到document.body再用position: fixed跟随当前目标移动。let button; function ensureButton() { if (button) return button; button document.createElement(button); button.id media-download-overlay; button.textContent 下载; document.body.appendChild(button); return button; }#media-download-overlay { position: fixed !important; z-index: 2147483647 !important; display: none; margin: 0 !important; }这个方案的好处有三个按钮脱离文档流不会挤压页面内容全局复用一个节点避免给每张图片重复绑定 UI新内容通过无限滚动加载后也无需重新批量注入按钮。为了减少样式冲突可以使用足够独特的 ID、明确重置关键属性。更彻底的隔离方案则是使用 Shadow DOM不过实现成本也会略高。七、媒体检测事件委托比全量扫描更轻最简单的入口是把事件监听挂在document上document.addEventListener(mouseover, (event) { const media event.target.closest?.(img, video); if (media) showButtonFor(media); });事件委托天然支持后续动态插入的节点因此通常不需要MutationObserver扫描整个页面。但遮罩层会带来一个限制如果鼠标命中的始终是覆盖在媒体上方的独立元素event.target和document.elementFromPoint()得到的都可能只是遮罩而不是下面的图片。更稳妥的补充方式包括从遮罩节点向父级查找再在容器内查询img, video对已知站点适配其卡片结构使用elementsFromPoint()获取坐标处的元素栈再筛选媒体元素对高频mousemove做节流避免影响页面性能。const stack document.elementsFromPoint(x, y); const media stack.find(el el.matches?.(img, video));这比声称elementFromPoint()能“穿透遮罩”更准确单数版本只返回最上层元素复数版本才能检查整条命中栈。八、过滤、定位与状态反馈如果所有小图标都弹出下载按钮插件很快会变成视觉噪音。因此需要设置尺寸门槛例如图片宽高至少 60px、视频至少 120px。按钮定位时还要根据视口边界进行修正function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function positionButton(media, button) { const rect media.getBoundingClientRect(); const left clamp(rect.left 8, 4, innerWidth - button.offsetWidth - 4); const top clamp(rect.top 8, 4, innerHeight - button.offsetHeight - 4); button.style.left ${left}px; button.style.top ${top}px; }点击之后不要让用户猜下载是否成功。可以用短暂的颜色和文案反馈下载已创建绿色 “已开始”调用失败红色 “失败”1.5 秒后恢复默认状态。这里最好写“已开始”而不是“已下载”。chrome.downloads.download()返回下载任务 ID只代表任务已创建文件是否最终完成还需要监听下载状态。九、后台下载与文件名清理内容脚本只发送必要信息chrome.runtime.sendMessage({ action: download, url, filename });后台负责校验消息并创建下载chrome.runtime.onMessage.addListener((message, sender, sendResponse) { if (message.action ! download) return; const safeFilename sanitizeFilename(message.filename); chrome.downloads.download({ url: message.url, filename: safeFilename, saveAs: false, conflictAction: uniquify }).then(id { sendResponse({ success: true, downloadId: id }); }).catch(error { sendResponse({ success: false, error: error.message }); }); return true; });文件名至少需要处理 Windows 不允许的字符并限制长度function sanitizeFilename(filename ) { return filename .replace(/[\\/:*?|]/g, _) .replace(/[. ]$/g, ) .replace(/\s/g, ) .trim() .slice(0, 180) || media_${Date.now()}; }此外还应在后台验证 URL 协议只接受预期的http:/https:不要盲目信任来自页面的消息参数。十、这个方案支持什么又不支持什么场景支持情况说明img src/currentSrc✅最稳定srcset响应式图片✅应解析并选择合适候选data-src懒加载图片✅需兼容站点属性命名video src/source✅仅限普通可访问 URLCDN 查询参数标记格式⚠️可以猜测最好结合响应类型blob:MSE 视频❌不是可直接下载的远程文件Canvas 内存绘制⚠️需要单独导出逻辑且可能受跨域污染限制临时签名或鉴权资源⚠️受 Cookie、Referer、有效期和权限影响DRM 内容❌不应绕过十一、回头看真正可复用的是这套设计这个插件没有复杂依赖核心代码也不长但它体现了几个很实用的浏览器扩展设计原则权限最小化只申请真正需要的权限和站点范围职责分离DOM 识别放在 content script下载放在 background事件委托动态页面无需反复扫描单实例 UI一个悬浮按钮复用到底明确失败边界直链能下blob:、鉴权和 DRM 不假装能下反馈说人话任务创建叫“已开始”完成状态另行监听。一句话总结浏览器扩展的关键不是“把页面上的东西扒下来”而是识别浏览器已经获得的合法资源并用最少权限、最小侵入完成用户明确授权的保存操作。如果你正在学习 Manifest V3这个项目很适合作为练手案例它同时涉及 DOM、事件委托、跨上下文消息、Service Worker、下载 API 和安全边界却又不需要引入复杂框架。开源地址https://github.com/LumeVault/jimeng-image