Compare commits

..

10 Commits

Author SHA1 Message Date
mula.liu 1fbb5d81a6 基本实现三类生成的文件去水印 2026-02-03 00:45:23 +08:00
Jad 552e6f6670 docs: add original author attribution and copyright notice 2026-01-16 12:31:02 +08:00
Jad 978e746f08 chore: add Cloudflare deployment configuration 2026-01-16 12:04:34 +08:00
Jayesh f9e717aa77
fix: footer links, dynamic copyright year, and security improvements (fixes #20) (#21)
* fix: resolve footer links, dynamic year, and security issues (fixes #20)

* fix: use relative path for locale files

---------

Co-authored-by: Jad <i@nocilol.me>
2026-01-11 21:18:08 +08:00
Jad 1bddf1f4f5 feat(userscript): suppress cross-origin prompts 2026-01-04 10:05:43 +08:00
Jad 5ede3dcaa5 docs: add note regarding fingerprinting protection 2025-12-26 18:14:05 +08:00
Jad 6b3eab4616 perf: optimize batch processing efficiency 2025-12-26 17:52:03 +08:00
Jad d0025877c4 feat: Add non-gemini generated image tips, and add a image lightbox (#4)
* wip: detect image not be made with gemini

* wip: multi image list style

* feat: add image lightbox
2025-12-23 00:18:46 +08:00
Jad 2da2b0c9ed feat(userscript): adapt watermark removal for Gemini share pages 2025-12-22 23:53:29 +08:00
Jad 1a5f346b3c style: fix typo 2025-12-22 23:53:29 +08:00
37 changed files with 2548 additions and 557 deletions

1
.gitignore vendored
View File

@ -16,3 +16,4 @@ yarn-error.log*
# local env files
.env*.local
.env
.gemini-clipboard/

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2025 Jad
Copyright (c) 2024 AllenK (Kwyshell)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -40,7 +40,8 @@ A high-performance, 100% client-side tool for removing Gemini AI watermarks. Bui
## ⚠️ Disclaimer
> **USE AT YOUR OWN RISK**
> [!WARNING]
> **USE AT YOUR OWN RISK**
>
> This tool modifies image files. While it is designed to work reliably, unexpected results may occur due to:
> - Variations in Gemini's watermark implementation
@ -49,6 +50,9 @@ A high-performance, 100% client-side tool for removing Gemini AI watermarks. Bui
>
> The author assumes no responsibility for any data loss, image corruption, or unintended modifications. By using this tool, you acknowledge that you understand these risks.
> [!NOTE]
> **Note**: Disabling any fingerprint defender extensions (e.g., Canvas Fingerprint Defender) to avoid processing errors. https://github.com/journey-ad/gemini-watermark-remover/issues/3
## Usage
### Online Website
@ -201,15 +205,17 @@ The author does not condone or encourage the misuse of this tool for copyright i
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THIS SOFTWARE.**
## License
## Credits
[MIT License](./LICENSE)
This project is a JavaScript port of the [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool) by Allen Kuo ([@allenk](https://github.com/allenk)).
The Reverse Alpha Blending method and calibrated watermark masks are based on the original work © 2024 AllenK (Kwyshell), licensed under MIT License.
## Related Links
- [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool)
- [Removing Gemini AI Watermarks: A Deep Dive into Reverse Alpha Blending](https://allenkuo.medium.com/removing-gemini-ai-watermarks-a-deep-dive-into-reverse-alpha-blending-bbbd83af2a3f)
## Credits
## License
This project is a JavaScript port of the [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool) C++ implementation.
[MIT License](./LICENSE)

View File

@ -40,6 +40,7 @@
## ⚠️ 使用需注意
> [!WARNING]
> **使用此工具产生的风险由用户自行承担**
>
> 本工具涉及对图像数据的修改。尽管在设计上力求处理结果的可靠性,但由于以下因素,仍可能产生非预期的处理结果:
@ -49,6 +50,9 @@
>
> 作者对任何形式的数据丢失、图像损坏或非预期的修改结果不承担法律责任。使用本工具即代表您已了解并接受上述风险。
> [!NOTE]
> 另请注意:使用此工具需禁用 Canvas 指纹防护扩展(如 Canvas Fingerprint Defender否则可能会导致处理结果错误。 https://github.com/journey-ad/gemini-watermark-remover/issues/3
## 使用方法
### 在线使用
@ -221,15 +225,17 @@ export class WatermarkEngine {
**本软件按“原样”提供,不提供任何形式(无论是明示或暗示)的保证。在任何情况下,作者均不对因使用本软件而产生的任何索赔、损害或其他责任承担任何义务。**
## 许可证
## 致谢
[MIT License](./LICENSE)
本项目是 [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool) 的 JavaScript 移植版本,原作者 Allen Kuo ([@allenk](https://github.com/allenk))
反向 Alpha 混合算法和用于校准的水印图像基于原作者的工作 © 2024 AllenK (Kwyshell),采用 MIT 许可证
## 相关链接
- [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool)
- [算法原理说明](https://allenkuo.medium.com/removing-gemini-ai-watermarks-a-deep-dive-into-reverse-alpha-blending-bbbd83af2a3f)
## 致谢
## 许可证
基于 [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool) C++ 版本移植
[MIT License](./LICENSE)

View File

@ -30,13 +30,14 @@ const userscriptBanner = `// ==UserScript==
// @name Gemini NanoBanana Watermark Remover
// @name:zh-CN Gemini NanoBanana 图片水印移除
// @namespace https://github.com/journey-ad
// @version 0.1.5
// @version 0.1.6
// @description Automatically removes watermarks from Gemini AI generated images
// @description:zh-CN 自动移除 Gemini AI 生成图像中的水印
// @icon https://www.google.com/s2/favicons?domain=gemini.google.com
// @author journey-ad
// @license MIT
// @match https://gemini.google.com/*
// @connect googleusercontent.com
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
@ -51,6 +52,7 @@ const copyAssetsPlugin = {
if (!existsSync('dist/i18n')) mkdirSync('dist/i18n', { recursive: true });
cpSync('src/i18n', 'dist/i18n', { recursive: true });
cpSync('public', 'dist', { recursive: true });
cpSync('node_modules/pdfjs-dist/build/pdf.worker.min.mjs', 'dist/pdf.worker.min.mjs');
} catch (err) {
console.error('❌ Asset copy failed:', err);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

BIN
data/IMG_8597.JPG 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
data/天王星.png 100644

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

View File

@ -15,7 +15,12 @@
"serve": "npx serve dist"
},
"dependencies": {
"jszip": "^3.10.1"
"canvas": "^3.2.1",
"exifr": "^7.1.3",
"jspdf": "^4.1.0",
"jszip": "^3.10.1",
"medium-zoom": "^1.1.0",
"pdfjs-dist": "^5.4.624"
},
"devDependencies": {
"esbuild": "^0.24.0"

File diff suppressed because it is too large Load Diff

View File

@ -1,266 +1,274 @@
<!DOCTYPE html>
<html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gemini Watermark Remover</title>
<meta name="description" content="Free Gemini watermark remover tool to remove watermarks from Gemini images. — safe, private, and 100% browser-based.">
<meta name="keywords" content="Gemini watermark remover, remove Gemini watermark, Gemini AI image editor, Google AI watermark remover, Gemini watermark fix, watermark cleaner tool, Gemini watermark eraser, free AI watermark remover" />
<title>Gemini Watermark Cleaner</title>
<meta name="description" content="Free Gemini watermark remover tool. Safe, private, and 100% browser-based.">
<link rel="icon" href="data:image/svg+xml,%3Csvg class='h-10 w-10 text-indigo-500' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236366f1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3l-1.9 5.8-5.8 1.9 5.8 1.9 1.9 5.8 1.9-5.8 5.8-1.9-5.8-1.9z'/%3E%3C/svg%3E">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#10B981',
'primary-hover': '#059669',
dark: '#1F2937',
background: 'var(--background)',
surface: 'var(--surface)',
card: 'var(--card)',
border: 'var(--border)',
primary: 'var(--primary)',
secondary: 'var(--secondary)',
accent: '#3B82F6',
},
boxShadow: {
'soft': '0 4px 20px -2px rgba(16, 185, 129, 0.1)',
'card': '0 0 20px rgba(0,0,0,0.05)'
fontFamily: {
sans: ['Inter', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
}
}
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; }
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #10B981; border-radius: 4px; }
.step-arrow::after {
:root {
--background: #ffffff;
--surface: #f9fafb;
--card: #ffffff;
--border: #e5e7eb;
--primary: #111827;
--secondary: #6b7280;
}
.dark {
--background: #050505;
--surface: #0A0A0A;
--card: #111111;
--border: #262626;
--primary: #EDEDED;
--secondary: #888888;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background);
color: var(--primary);
}
.noise-layer {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
opacity: 0.04;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
}
.glass-nav {
backdrop-filter: blur(12px);
}
.dark .glass-nav {
background: rgba(5, 5, 5, 0.8);
border-bottom: 1px solid #262626;
}
.light .glass-nav {
background: rgba(255, 255, 255, 0.8);
border-bottom: 1px solid #e5e7eb;
}
.spotlight-wrapper {
position: relative;
background: rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1px;
box-shadow: 0 0 0 1px rgba(0,0,0,1), 0 20px 40px -20px rgba(0,0,0,0.5);
overflow: hidden;
}
.light .spotlight-wrapper {
background: rgba(0, 0, 0, 0.05);
box-shadow: 0 0 0 1px rgba(0,0,0,0.05), 0 20px 40px -20px rgba(0,0,0,0.1);
}
.drop-zone {
position: relative;
}
.dark .drop-zone {
background: #0A0A0A;
background-image: linear-gradient(#262626 1px, transparent 1px), linear-gradient(90deg, #262626 1px, transparent 1px);
background-size: 40px 40px;
}
.light .drop-zone {
background: #ffffff;
background-image: linear-gradient(#f3f4f6 1px, transparent 1px), linear-gradient(90deg, #f3f4f6 1px, transparent 1px);
background-size: 40px 40px;
}
.dark .drop-zone::after {
content: '';
position: absolute;
right: -10px;
top: 50%;
transform: translateY(-50%);
border-top: 15px solid transparent;
border-bottom: 15px solid transparent;
border-left: 15px solid #F3F4F6;
z-index: 10;
inset: 0;
background: radial-gradient(circle at center, transparent 20%, #0A0A0A 100%);
pointer-events: none;
}
@media (min-width: 768px) {
.step-item:not(:last-child)::after {
content: '';
position: absolute;
right: -24px;
top: 0;
bottom: 0;
width: 24px;
background-color: inherit;
clip-path: polygon(0% 0%, 100% 50%, 0% 100%);
z-index: 20;
}
.step-item:not(:first-child) {
clip-path: polygon(0% 0%, 200% 0%, 100% 100%, 0% 100%, 24px 50%);
}
.light .drop-zone::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at center, transparent 20%, #ffffff 100%);
pointer-events: none;
}
.preview-bg {
background-size: 20px 20px;
}
.dark .preview-bg {
background-color: #111;
background-image: radial-gradient(#333 1px, transparent 1px);
}
.light .preview-bg {
background-color: #f9fafb;
background-image: radial-gradient(#e5e7eb 1px, transparent 1px);
}
/* Ensure zoom preview is on top of everything */
.medium-zoom-overlay,
.medium-zoom-image--opened {
z-index: 9999 !important;
}
body.loading { opacity: 0; }
body { transition: opacity 0s; }
</style>
</head>
<body class="bg-white text-gray-800 antialiased selection:bg-primary selection:text-white flex flex-col min-h-screen loading">
<body class="min-h-screen flex flex-col relative overflow-x-hidden bg-background text-primary dark:bg-background dark:text-primary">
<div class="noise-layer"></div>
<header class="sticky top-0 z-50 bg-white/90 backdrop-blur-md border-b border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div class="flex items-center gap-1">
<svg class="h-10 w-10 text-indigo-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m12 3-1.9 5.8-5.8 1.9 5.8 1.9 1.9 5.8 1.9-5.8 5.8-1.9-5.8-1.9Z"></path>
</svg>
<h1 class="text-sm font-bold tracking-tight text-primary md:text-xl">
<a href="/" class="hover:text-primary transition-colors" title="Gemini Watermark Remover">Gemini <span class="text-gray-700 font-medium">Watermark Remover</span></a>
</h1>
</div>
<nav class="flex gap-2 md:gap-6 text-sm font-medium text-gray-600 items-center">
<a href="/userscript/gemini-watermark-remover.user.js" target="_blank" class="hidden md:inline-block hover:text-primary transition-colors" data-i18n="nav.userscript">油猴脚本</a>
<a href="https://allenkuo.medium.com/removing-gemini-ai-watermarks-a-deep-dive-into-reverse-alpha-blending-bbbd83af2a3f" target="_blank" class="hidden md:inline-block hover:text-primary transition-colors" data-i18n="nav.principle">去水印原理</a>
<a href="https://github.com/journey-ad/gemini-watermark-remover" target="_blank" class="hover:text-primary transition-colors">GitHub</a>
<button id="langSwitch" class="px-3 py-1 text-nowrap border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">EN</button>
</nav>
<header class="glass-nav sticky top-0 z-50 h-16 flex items-center justify-between px-6 lg:px-12">
<div class="flex items-center gap-3">
<div class="w-2 h-2 bg-primary rounded-sm"></div>
<h1 class="font-mono text-sm font-medium tracking-tight">GEMINI WATERMARK CLEANER</h1>
</div>
<nav class="flex items-center gap-6 text-xs md:text-sm font-medium text-secondary">
<button id="themeToggle" class="p-2 border border-border rounded hover:bg-surface text-primary transition-colors">
<svg id="themeIcon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path id="themeIconPath" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
</svg>
</button>
<a href="https://github.com/journey-ad/gemini-watermark-remover" target="_blank" class="hover:text-primary transition-colors">GitHub</a>
<button id="langSwitch" class="px-3 py-1 border border-border rounded hover:bg-surface text-primary transition-colors">EN</button>
</nav>
</header>
<main class="flex-grow">
<section class="relative pt-16 pb-12 lg:pt-24 lg:pb-20 text-center px-4 overflow-hidden">
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full h-full bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-emerald-50 via-white to-white -z-10"></div>
<h2 class="bg-clip-text bg-gradient-to-br font-extrabold from-slate-900 mb-6 md:text-6xl text-3xl text-transparent to-slate-700 tracking-tighter" data-i18n="main.title">
Gemini AI 图像去水印
<main class="flex-grow container mx-auto px-4 py-12 md:py-20 z-10 relative max-w-5xl">
<!-- Hero Section -->
<section class="text-center mb-16 space-y-6">
<h2 class="text-4xl md:text-6xl font-medium tracking-tight bg-clip-text text-transparent bg-gradient-to-b from-primary to-secondary" data-i18n="main.title">
Restore Clarity.
</h2>
<p class="text-base md:text-lg text-gray-500 max-w-2xl mx-auto mb-10" data-i18n="main.subtitle">
基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损
<p class="text-secondary text-base md:text-lg max-w-2xl mx-auto font-light" data-i18n="main.subtitle">
Remove Gemini & NotebookLM watermarks instantly. Local processing, privacy-first.
</p>
</section>
<div class="max-w-4xl mx-auto bg-white rounded-2xl shadow-soft p-2 md:p-3 border border-emerald-100">
<div id="uploadArea" class="group relative flex flex-col items-center justify-center w-full h-64 border-2 border-dashed border-emerald-200 rounded-xl bg-emerald-50/30 hover:bg-emerald-50 hover:border-emerald-400 transition-all cursor-pointer">
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<div class="w-16 h-16 bg-white rounded-full flex items-center justify-center shadow-sm mb-4 group-hover:scale-110 transition-transform duration-300">
<svg class="w-8 h-8 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg>
<!-- Upload Area -->
<section class="mb-20">
<div class="spotlight-wrapper max-w-2xl mx-auto group">
<div id="uploadArea" class="drop-zone h-64 md:h-80 rounded-xl flex flex-col items-center justify-center cursor-pointer transition-colors hover:border-accent/50">
<div class="z-10 flex flex-col items-center gap-4 transition-transform group-hover:-translate-y-1 duration-300">
<div class="w-12 h-12 rounded-xl bg-card border border-border flex items-center justify-center text-secondary">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path></svg>
</div>
<div class="text-center">
<p class="text-primary font-medium mb-1" data-i18n="upload.text">Drop files here or click to upload</p>
<p class="text-secondary text-xs font-mono bg-surface px-2 py-1 rounded" data-i18n="upload.hint">JPG, PNG, WEBP, PDF</p>
</div>
<p class="mb-2 text-lg font-medium text-gray-700" data-i18n="upload.text">点击选择 或 拖拽图片至此</p>
<p class="text-sm text-gray-400" data-i18n="upload.hint">支持 JPG, PNG, WebP (无文件大小限制)</p>
</div>
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/webp" multiple class="hidden" />
<input type="file" id="fileInput" accept="image/jpeg,image/png,image/webp,application/pdf" multiple class="hidden" />
</div>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 mb-8 md:mb-20">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-10 text-center">
<div class="step-item relative bg-emerald-50 h-16 md:h-24 flex items-center justify-start px-6 md:pl-16 rounded-lg md:rounded-none md:first:rounded-l-lg md:last:rounded-r-lg">
<div class="w-8 h-8 rounded-full bg-emerald-200 text-emerald-800 font-bold flex items-center justify-center mr-3 text-sm">1</div>
<span class="font-medium flex-1 md:flex-none text-gray-800" data-i18n="step.1">选择原始图片</span>
</div>
<div class="step-item relative bg-emerald-50 h-16 md:h-24 flex items-center justify-start px-6 md:pl-16 rounded-lg md:rounded-none">
<div class="w-8 h-8 rounded-full bg-emerald-200 text-emerald-800 font-bold flex items-center justify-center mr-3 text-sm">2</div>
<span class="font-medium flex-1 md:flex-none text-gray-800" data-i18n="step.2">算法自动解析</span>
</div>
<div class="step-item relative bg-emerald-50 h-16 md:h-24 flex items-center justify-start px-6 md:pl-16 rounded-lg md:rounded-none md:last:rounded-r-lg">
<div class="w-8 h-8 rounded-full bg-emerald-200 text-emerald-800 font-bold flex items-center justify-center mr-3 text-sm">3</div>
<span class="font-medium flex-1 md:flex-none text-gray-800" data-i18n="step.3">保存无水印图</span>
</div>
</div>
</section>
<section id="singlePreview" class="max-w-7xl mx-auto px-4 pb-24" style="display: none;">
<div class="flex flex-col lg:flex-row gap-8">
<div class="flex-1 space-y-8">
<div class="bg-white rounded-2xl shadow-card overflow-hidden border border-gray-100">
<div class="bg-gray-50 px-6 py-3 border-b border-gray-100 flex justify-between items-center">
<h3 class="font-semibold text-gray-700 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-red-400"></span> <span data-i18n="preview.original">原图预览</span>
</h3>
<span id="originalInfo" class="text-xs text-gray-400 font-mono"></span>
</div>
<div class="p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]">
<canvas id="originalCanvas" class="max-w-full h-auto mx-auto rounded-lg shadow-sm block"></canvas>
</div>
<!-- Previews -->
<section id="singlePreview" class="hidden mb-20 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div class="grid lg:grid-cols-2 gap-6">
<!-- Original -->
<div class="bg-card border border-border rounded-xl overflow-hidden">
<div class="px-4 py-3 border-b border-border flex justify-between items-center bg-surface">
<span class="text-xs font-mono text-secondary" data-i18n="preview.original">ORIGINAL</span>
<span id="originalInfo" class="text-[10px] font-mono text-secondary/50"></span>
</div>
<div id="processedSection" class="bg-white rounded-2xl shadow-card overflow-hidden border border-gray-100 ring-4 ring-emerald-50 scroll-mt-24" style="display: none;">
<div class="bg-emerald-50/50 px-6 py-3 border-b border-emerald-100 flex justify-between items-center">
<h3 class="font-semibold text-emerald-800 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-primary"></span> <span data-i18n="preview.result">处理结果</span>
</h3>
<span id="processedInfo" class="text-xs text-emerald-600 font-mono"></span>
</div>
<div class="p-4 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iI2Y5ZmRmZCI+PHJlY3Qgd2lkdGg9IjEwIiBoZWlnaHQ9IjEwIiBmaWxsPSIjZjJmMmYyIi8+PHJlY3QgeD0iMTAiIHk9IjEwIiB3aWR0aD0iMTAiIGhlaWdodD0iMTAiIGZpbGw9IiNmMmYyZjIiLz48L3N2Zz4=')]">
<img id="processedImage" class="max-w-full h-auto mx-auto rounded-lg shadow-sm block" />
</div>
<div class="h-64 md:h-96 preview-bg flex items-center justify-center p-4">
<img id="originalImage" class="max-w-full max-h-full rounded shadow-lg" data-zoomable />
</div>
</div>
<div class="w-full lg:w-80 flex-shrink-0">
<div class="bg-white rounded-2xl shadow-lg border border-gray-100 p-6 sticky top-24">
<h4 class="text-lg font-bold text-gray-900 mb-6" data-i18n="panel.title">操作面板</h4>
<div class="space-y-3">
<button id="downloadBtn" class="w-full py-3.5 px-4 bg-gray-900 hover:bg-black text-white rounded-xl font-medium transition-all flex items-center justify-center gap-2" style="display: none;">
<svg class="w-5 h-5" 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"></path></svg>
<span data-i18n="btn.download">下载结果</span>
</button>
<button id="resetBtn" class="w-full py-3.5 px-4 bg-white border border-gray-200 text-gray-600 hover:bg-gray-50 hover:text-gray-900 rounded-xl font-medium transition-all flex items-center justify-center gap-2">
<svg class="w-5 h-5" 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"></path></svg>
<span data-i18n="btn.reset">重置 / 更换图片</span>
</button>
</div>
<div id="statusMessage" class="mt-6 text-sm text-center text-gray-500 min-h-[1.25rem]"></div>
<!-- Result -->
<div id="processedSection" class="bg-card border border-border rounded-xl overflow-hidden hidden">
<div class="px-4 py-3 border-b border-border flex justify-between items-center bg-surface">
<span class="text-xs font-mono text-accent" data-i18n="preview.result">CLEANED</span>
<span id="processedInfo" class="text-[10px] font-mono text-secondary/50"></span>
</div>
<div class="h-64 md:h-96 preview-bg flex items-center justify-center p-4 relative">
<img id="processedImage" class="max-w-full max-h-full rounded shadow-lg" data-zoomable />
</div>
</div>
</div>
</section>
<section id="multiPreview" class="max-w-7xl mx-auto px-4 pb-24 scroll-mt-24" style="display: none;">
<div class="mb-6 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900">
<span id="progressText">处理进度: 0/0</span>
</h3>
<button id="downloadAllBtn" class="py-2.5 px-6 bg-gray-900 hover:bg-gray-800 text-white rounded-xl font-medium transition-all flex items-center gap-2" style="display: none;">
<svg class="w-5 h-5" 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"></path></svg>
<span data-i18n="btn.downloadAll">全部下载</span>
<!-- Actions -->
<div class="flex flex-col sm:flex-row justify-center gap-4 pt-4">
<button id="resetBtn" class="px-6 py-2.5 rounded-lg border border-border text-secondary hover:text-primary hover:border-secondary transition-all text-sm font-medium" data-i18n="btn.reset">
Reset
</button>
<button id="downloadBtn" class="px-6 py-2.5 rounded-lg bg-primary text-background hover:opacity-90 transition-all text-sm font-medium flex items-center justify-center gap-2 hidden" data-i18n="btn.download">
Download Result
</button>
</div>
<div id="imageList" class="space-y-4"></div>
<div id="statusMessage" class="text-center text-xs font-mono text-secondary h-4"></div>
</section>
<section class="bg-gray-50 py-16 border-t border-gray-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-6 md:mb-12">
<h3 class="text-2xl font-bold text-gray-900" data-i18n="feature.title">功能特点</h3>
<!-- Multi Preview -->
<section id="multiPreview" class="hidden mb-20">
<div class="flex justify-between items-end mb-6 border-b border-border pb-4">
<span id="progressText" class="font-mono text-sm text-secondary">QUEUE</span>
<button id="downloadAllBtn" class="text-xs font-mono bg-primary text-background px-4 py-2 rounded hover:opacity-90 transition-colors hidden" data-i18n="btn.downloadAll">DOWNLOAD ALL</button>
</div>
<div id="imageList" class="grid grid-cols-1 md:grid-cols-2 gap-4"></div>
</section>
<!-- Features -->
<section class="grid grid-cols-1 md:grid-cols-3 gap-6 pt-12 border-t border-border">
<div class="p-6 rounded-xl bg-card border border-border hover:border-secondary transition-colors">
<div class="w-8 h-8 text-primary mb-4">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-8">
<div class="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="w-12 h-12 bg-emerald-100 text-primary rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
</div>
<h4 class="text-lg font-bold text-gray-900 mb-2" data-i18n="feature.speed.title">极速处理</h4>
<p class="text-gray-500 text-sm leading-relaxed" data-i18n="feature.speed.desc">基于现代浏览器技术加速处理,毫秒级响应,无需等待排队</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6" 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"></path></svg>
</div>
<h4 class="text-lg font-bold text-gray-900 mb-2" data-i18n="feature.privacy.title">隐私安全</h4>
<p class="text-gray-500 text-sm leading-relaxed" data-i18n="feature.privacy.desc">纯前端运行,图片数据不离机,绝不上传服务器</p>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<div class="w-12 h-12 bg-purple-100 text-purple-600 rounded-lg flex items-center justify-center mb-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z M6 6l12 12"></path>
</svg>
</div>
<h4 class="text-lg font-bold text-gray-900 mb-2" data-i18n="feature.free.title">完全免费</h4>
<p class="text-gray-500 text-sm leading-relaxed" data-i18n="feature.free.desc">无次数限制,无隐藏付费,即开即用</p>
</div>
<h3 class="font-medium mb-2 text-sm text-primary" data-i18n="feature.speed.title">Local Processing</h3>
<p class="text-xs text-secondary leading-relaxed" data-i18n="feature.speed.desc">Runs entirely in your browser. No data leaves your device.</p>
</div>
<div class="p-6 rounded-xl bg-card border border-border hover:border-secondary transition-colors">
<div class="w-8 h-8 text-primary mb-4">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
</div>
<h3 class="font-medium mb-2 text-sm text-primary" data-i18n="feature.privacy.title">Privacy First</h3>
<p class="text-xs text-secondary leading-relaxed" data-i18n="feature.privacy.desc">Zero server uploads. Your intellectual property stays yours.</p>
</div>
<div class="p-6 rounded-xl bg-card border border-border hover:border-secondary transition-colors">
<div class="w-8 h-8 text-primary mb-4">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<h3 class="font-medium mb-2 text-sm text-primary" data-i18n="feature.free.title">Forever Free</h3>
<p class="text-xs text-secondary leading-relaxed" data-i18n="feature.free.desc">Open source and free to use. No hidden fees or limits.</p>
</div>
</section>
</main>
<footer class="bg-emerald-600 text-white py-6 md:pt-12 md:pb-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 md:gap-8 mb-8">
<div class="col-span-1 md:col-span-2">
<h5 class="text-2xl font-bold mb-4" data-i18n="header.title">Gemini Watermark Remover</h5>
<p class="text-emerald-100 text-sm max-w-sm" data-i18n="footer.desc">
Gemini 无损去水印工具,本工具仅供学习交流使用
</p>
</div>
<div>
<h6 class="font-bold mb-2 md:mb-4 text-emerald-100" data-i18n="footer.links">链接</h6>
<ul class="space-y-1 md:space-y-2 text-sm text-emerald-50">
<li><a href="/terms.html" class="hover:text-white" data-i18n="footer.terms">使用条款</a></li>
<li><a href="https://github.com/journey-ad/gemini-watermark-web" target="_blank" class="hover:text-white">Github</a></li>
<li><a href="/userscript/gemini-watermark-remover.user.js" target="_blank" class="hover:text-white" data-i18n="nav.userscript">油猴脚本</a></li>
</ul>
</div>
<div>
<h6 class="font-bold mb-2 md:mb-4 text-emerald-100" data-i18n="footer.tech">技术</h6>
<ul class="space-y-1 md:space-y-2 text-sm text-emerald-50">
<li><a href="https://allenkuo.medium.com/removing-gemini-ai-watermarks-a-deep-dive-into-reverse-alpha-blending-bbbd83af2a3f" target="_blank" class="hover:text-white" data-i18n="nav.principle">去水印原理</a></li>
</ul>
</div>
<footer class="border-t border-border mt-auto">
<div class="container mx-auto px-6 py-8 flex flex-col md:flex-row justify-between items-center text-xs text-secondary font-mono">
<div class="mb-4 md:mb-0">
<span data-i18n="footer.copyright">&copy; 2025 Gemini Watermark Remover</span>
</div>
<div class="border-t border-emerald-500 pt-8 text-center text-sm text-emerald-200">
<p data-i18n="footer.copyright">&copy; 2025 Gemini Watermark Remover. All rights reserved.</p>
<div class="flex gap-6">
<a href="terms.html" class="hover:text-primary transition-colors" data-i18n="footer.terms">Terms</a>
<a href="https://github.com/journey-ad/gemini-watermark-remover" target="_blank" class="hover:text-primary transition-colors">GitHub</a>
</div>
</div>
</footer>
<div id="loadingOverlay" class="fixed inset-0 bg-white/80 backdrop-blur-sm z-[60] hidden flex items-center justify-center">
<div class="flex flex-col items-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mb-4"></div>
<p class="text-gray-600 font-medium" data-i18n="loading.text">正在处理...</p>
<div id="loadingOverlay" class="fixed inset-0 bg-background/80 backdrop-blur-sm z-[100] hidden flex items-center justify-center">
<div class="flex flex-col items-center gap-4">
<div class="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="text-xs font-mono text-secondary" data-i18n="loading.text">PROCESSING</p>
</div>
</div>

View File

@ -36,7 +36,7 @@
<path d="m12 3-1.9 5.8-5.8 1.9 5.8 1.9 1.9 5.8 1.9-5.8 5.8-1.9-5.8-1.9Z"></path>
</svg>
<h1 class="text-sm font-bold tracking-tight text-primary md:text-xl">
<a href="/" class="hover:text-primary transition-colors" title="Gemini Watermark Remover">Gemini <span class="text-gray-700 font-medium">Watermark Remover</span></a>
<a href="./" class="hover:text-primary transition-colors" title="Gemini Watermark Remover">Gemini <span class="text-gray-700 font-medium">Watermark Remover</span></a>
</h1>
</div>
</div>
@ -129,10 +129,13 @@
<footer class="bg-emerald-600 text-white pt-12 pb-8 mt-auto">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center text-sm text-emerald-200">
<p>&copy; 2025 Gemini Watermark Remover. All rights reserved.</p>
<p>&copy; <span id="year">2025</span> Gemini Watermark Remover. All rights reserved.</p>
</div>
</div>
</footer>
<script>
document.getElementById('year').textContent = new Date().getFullYear();
</script>
</body>
</html>

View File

@ -1,11 +1,18 @@
import { WatermarkEngine } from './core/watermarkEngine.js';
import i18n from './i18n.js';
import { loadImage, checkOriginal, getOriginalStatus, setStatusMessage, showLoading, hideLoading } from './utils.js';
import JSZip from 'jszip';
import mediumZoom from 'medium-zoom';
import * as pdfjsLib from 'pdfjs-dist';
import { jsPDF } from 'jspdf';
pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.min.mjs';
// global state
let engine = null;
let imageQueue = [];
let processedCount = 0;
let zoom = null;
// dom elements references
const uploadArea = document.getElementById('uploadArea');
@ -15,15 +22,13 @@ const multiPreview = document.getElementById('multiPreview');
const imageList = document.getElementById('imageList');
const progressText = document.getElementById('progressText');
const downloadAllBtn = document.getElementById('downloadAllBtn');
const loadingOverlay = document.getElementById('loadingOverlay');
const originalCanvas = document.getElementById('originalCanvas');
const originalImage = document.getElementById('originalImage');
const processedSection = document.getElementById('processedSection');
const processedImage = document.getElementById('processedImage');
const originalInfo = document.getElementById('originalInfo');
const processedInfo = document.getElementById('processedInfo');
const downloadBtn = document.getElementById('downloadBtn');
const resetBtn = document.getElementById('resetBtn');
const statusMessage = document.getElementById('statusMessage');
/**
* initialize the application
@ -32,15 +37,63 @@ async function init() {
try {
await i18n.init();
setupLanguageSwitch();
setupThemeToggle();
showLoading(i18n.t('status.loading'));
engine = await WatermarkEngine.create();
hideLoading();
setupEventListeners();
zoom = mediumZoom('[data-zoomable]', {
margin: 24,
scrollOffset: 0,
background: 'rgba(255, 255, 255, .6)',
})
} catch (error) {
hideLoading();
console.error('初始化错误:', error);
console.error('initialize error:', error);
}
}
/**
* setup theme toggle
*/
function setupThemeToggle() {
const btn = document.getElementById('themeToggle');
const html = document.documentElement;
const iconPath = document.getElementById('themeIconPath');
const sunPath = 'M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M16 12a4 4 0 11-8 0 4 4 0 018 0z';
const moonPath = 'M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z';
// Initialize based on current class
if (html.classList.contains('dark')) {
iconPath.setAttribute('d', sunPath);
} else {
iconPath.setAttribute('d', moonPath);
}
btn.addEventListener('click', () => {
if (html.classList.contains('dark')) {
html.classList.remove('dark');
html.classList.add('light');
iconPath.setAttribute('d', moonPath);
localStorage.setItem('theme', 'light');
} else {
html.classList.remove('light');
html.classList.add('dark');
iconPath.setAttribute('d', sunPath);
localStorage.setItem('theme', 'dark');
}
});
// Load from storage
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
html.classList.remove('dark');
html.classList.add('light');
iconPath.setAttribute('d', moonPath);
}
}
@ -98,20 +151,28 @@ function handleFileSelect(e) {
function handleFiles(files) {
const validFiles = files.filter(file => {
if (!file.type.match('image/(jpeg|png|webp)')) return false;
if (file.size > 20 * 1024 * 1024) return false;
if (!file.type.match('image/(jpeg|png|webp)') && file.type !== 'application/pdf') return false;
if (file.size > 50 * 1024 * 1024) return false; // Increased limit for PDF
return true;
});
if (validFiles.length === 0) return;
imageQueue.forEach(item => {
if (item.originalUrl) URL.revokeObjectURL(item.originalUrl);
if (item.processedUrl) URL.revokeObjectURL(item.processedUrl);
});
imageQueue = validFiles.map((file, index) => ({
id: Date.now() + index,
file,
name: file.name,
isPdf: file.type === 'application/pdf',
status: 'pending',
originalImg: null,
processedBlob: null
processedBlob: null,
originalUrl: null,
processedUrl: null
}));
processedCount = 0;
@ -131,36 +192,91 @@ function handleFiles(files) {
}
}
async function getPdfPreview(file) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
const img = new Image();
img.src = canvas.toDataURL();
return new Promise(resolve => {
img.onload = () => resolve(img);
});
}
async function processSingle(item) {
try {
const img = await loadImage(item.file);
item.originalImg = img;
let img;
if (item.isPdf) {
img = await getPdfPreview(item.file);
item.originalImg = img;
originalInfo.innerHTML = `
<p>Type: PDF (NotebookLM)</p>
<p>${i18n.t('info.size')}: ${(item.file.size / 1024 / 1024).toFixed(2)} MB</p>
`;
setStatusMessage('NotebookLM PDF Detected', 'success');
} else {
img = await loadImage(item.file);
item.originalImg = img;
originalCanvas.width = img.width;
originalCanvas.height = img.height;
originalCanvas.getContext('2d').drawImage(img, 0, 0);
const { is_google, is_original, sourceType } = await checkOriginal(item.file);
const status = getOriginalStatus({ is_google, is_original, sourceType });
setStatusMessage(status, is_google && is_original ? 'success' : 'warn');
// update original image info
const watermarkInfo = engine.getWatermarkInfo(img.width, img.height);
originalInfo.innerHTML = `
<strong>${i18n.t('info.size')}</strong>${img.width} × ${img.height} px<br>
<strong>${i18n.t('info.watermark')}</strong>${watermarkInfo.size}×${watermarkInfo.size} px<br>
<strong>${i18n.t('info.position')}</strong>(${watermarkInfo.position.x}, ${watermarkInfo.position.y})
`;
const watermarkInfo = engine.getWatermarkInfo(img.width, img.height);
originalInfo.innerHTML = `
<p>${i18n.t('info.size')}: ${img.width}×${img.height}</p>
<p>${i18n.t('info.watermark')}: ${watermarkInfo.width}×${watermarkInfo.height}</p>
<p>${i18n.t('info.position')}: (${watermarkInfo.position.x},${watermarkInfo.position.y})</p>
`;
}
const result = await engine.removeWatermarkFromImage(img);
const blob = await new Promise(resolve => result.toBlob(resolve, 'image/png'));
originalImage.src = img.src;
let blob;
let previewUrl;
if (item.isPdf) {
const result = await processPdf(item.file);
blob = result.pdfBlob;
previewUrl = result.previewDataUrl;
} else {
// Standard Gemini processing (Alpha Map)
const result = await engine.removeWatermarkFromImage(img);
blob = await new Promise(resolve => result.toBlob(resolve, 'image/png'));
previewUrl = URL.createObjectURL(blob);
}
item.processedBlob = blob;
processedImage.src = URL.createObjectURL(blob);
item.processedUrl = URL.createObjectURL(blob);
// Use the cleaned preview (for PDF it's the first page cleaned, for image it's the image itself)
processedImage.src = previewUrl;
processedSection.style.display = 'block';
downloadBtn.style.display = 'flex';
downloadBtn.onclick = () => downloadImage(item);
processedInfo.innerHTML = `
<strong>${i18n.t('info.size')}</strong>${img.width} × ${img.height} px<br>
<strong>${i18n.t('info.status')}</strong>${i18n.t('info.removed')}
`;
if (item.isPdf) {
processedInfo.innerHTML = `
<p>${i18n.t('info.status')}: ${i18n.t('info.removed')}</p>
<p>Output: PDF</p>
`;
} else {
processedInfo.innerHTML = `
<p>${i18n.t('info.size')}: ${img.width}×${img.height}</p>
<p>${i18n.t('info.status')}: ${i18n.t('info.removed')}</p>
`;
}
zoom.detach();
zoom.attach('[data-zoomable]');
processedSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
} catch (error) {
@ -171,20 +287,20 @@ async function processSingle(item) {
function createImageCard(item) {
const card = document.createElement('div');
card.id = `card-${item.id}`;
card.className = 'bg-white md:h-[130px] rounded-xl shadow-card border border-gray-100 overflow-hidden';
card.className = 'bg-white md:h-[140px] rounded-xl shadow-card border border-gray-100 overflow-hidden';
card.innerHTML = `
<div class="flex flex-wrap h-full relative">
<div class="w-full md:w-auto h-full flex">
<div class="flex flex-wrap h-full">
<div class="w-full md:w-auto h-full flex border-b border-gray-100">
<div class="w-24 md:w-48 flex-shrink-0 bg-gray-50 p-2 flex items-center justify-center">
<img id="result-${item.id}" class="max-w-full max-h-full rounded"></img>
<img id="result-${item.id}" class="max-w-full max-h-24 md:max-h-full rounded" data-zoomable />
</div>
<div class="flex-1 p-4 flex flex-col min-w-0">
<h4 class="font-semibold text-gray-900 mb-2 truncate">${item.name}</h4>
<h4 class="font-semibold text-sm text-gray-900 mb-2 truncate">${item.name}</h4>
<div class="text-xs text-gray-500" id="status-${item.id}">${i18n.t('status.pending')}</div>
</div>
</div>
<div class="absolute bottom-0 right-0 md:static w-auto ml-auto flex-shrink-0 p-2 md:p-4 flex items-center justify-center">
<button id="download-${item.id}" class="px-4 py-2 bg-gray-900 hover:bg-gray-800 text-white rounded-lg text-sm hidden">${i18n.t('btn.download')}</button>
<div class="w-full md:w-auto ml-auto flex-shrink-0 p-2 md:p-4 flex items-center justify-center">
<button id="download-${item.id}" class="px-4 py-2 bg-gray-900 hover:bg-gray-800 text-white rounded-lg text-xs md:text-sm hidden">${i18n.t('btn.download')}</button>
</div>
</div>
`;
@ -192,42 +308,82 @@ function createImageCard(item) {
}
async function processQueue() {
for (const item of imageQueue) {
const img = await loadImage(item.file);
item.originalImg = img;
document.getElementById(`result-${item.id}`).src = img.src;
}
for (const item of imageQueue) {
if (item.status !== 'pending') continue;
item.status = 'processing';
updateStatus(item.id, i18n.t('status.processing'));
try {
const result = await engine.removeWatermarkFromImage(item.originalImg);
const blob = await new Promise(resolve => result.toBlob(resolve, 'image/png'));
item.processedBlob = blob;
document.getElementById(`result-${item.id}`).src = URL.createObjectURL(blob);
item.status = 'completed';
const watermarkInfo = engine.getWatermarkInfo(item.originalImg.width, item.originalImg.height);
updateStatus(item.id, `<strong>${i18n.t('info.size')}</strong>${item.originalImg.width} × ${item.originalImg.height} px<br>
<strong>${i18n.t('info.watermark')}</strong>${watermarkInfo.size}×${watermarkInfo.size} px<br>
<strong>${i18n.t('info.position')}</strong>(${watermarkInfo.position.x}, ${watermarkInfo.position.y})`, true);
const downloadBtn = document.getElementById(`download-${item.id}`);
downloadBtn.classList.remove('hidden');
downloadBtn.onclick = () => downloadImage(item);
processedCount++;
updateProgress();
} catch (error) {
item.status = 'error';
updateStatus(item.id, i18n.t('status.failed'));
console.error(error);
// Pre-load images/previews
await Promise.all(imageQueue.map(async item => {
let img;
if (item.isPdf) {
img = await getPdfPreview(item.file);
} else {
img = await loadImage(item.file);
}
item.originalImg = img;
item.originalUrl = img.src;
document.getElementById(`result-${item.id}`).src = img.src;
zoom.attach(`#result-${item.id}`);
}));
const concurrency = 3;
for (let i = 0; i < imageQueue.length; i += concurrency) {
await Promise.all(imageQueue.slice(i, i + concurrency).map(async item => {
if (item.status !== 'pending') return;
item.status = 'processing';
updateStatus(item.id, i18n.t('status.processing'));
try {
let blob;
let previewUrl;
if (item.isPdf) {
const res = await processPdf(item.file, (page, total) => {
updateStatus(item.id, `Processing page ${page}/${total}...`);
});
blob = res.pdfBlob;
previewUrl = res.previewDataUrl;
} else {
const result = await engine.removeWatermarkFromImage(item.originalImg);
blob = await new Promise(resolve => result.toBlob(resolve, 'image/png'));
previewUrl = URL.createObjectURL(blob);
}
item.processedBlob = blob;
item.processedUrl = URL.createObjectURL(blob);
// Update with cleaned preview
document.getElementById(`result-${item.id}`).src = previewUrl;
item.status = 'completed';
if (item.isPdf) {
updateStatus(item.id, `PDF Processed`, true);
} else {
const watermarkInfo = engine.getWatermarkInfo(item.originalImg.width, item.originalImg.height);
updateStatus(item.id, `<p>${i18n.t('info.size')}: ${item.originalImg.width}×${item.originalImg.height}</p>
<p>${i18n.t('info.watermark')}: ${watermarkInfo.width}×${watermarkInfo.height}</p>
<p>${i18n.t('info.position')}: (${watermarkInfo.position.x},${watermarkInfo.position.y})</p>`, true);
}
const downloadBtn = document.getElementById(`download-${item.id}`);
downloadBtn.classList.remove('hidden');
downloadBtn.onclick = () => downloadImage(item);
processedCount++;
updateProgress();
if (!item.isPdf) {
checkOriginal(item.file).then(({ is_google, is_original, sourceType }) => {
if (!is_google || !is_original) {
const status = getOriginalStatus({ is_google, is_original, sourceType });
const statusEl = document.getElementById(`status-${item.id}`);
if (statusEl) statusEl.innerHTML += `<p class="inline-block mt-1 text-xs md:text-sm text-warn">${status}</p>`;
}
}).catch(() => {});
}
} catch (error) {
item.status = 'error';
updateStatus(item.id, i18n.t('status.failed'));
console.error(error);
}
}));
}
if (processedCount > 0) {
@ -235,20 +391,6 @@ async function processQueue() {
}
}
function loadImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function updateStatus(id, text, isHtml = false) {
const el = document.getElementById(`status-${id}`);
if (el) el.innerHTML = isHtml ? text : text.replace(/\n/g, '<br>');
@ -266,8 +408,11 @@ function updateDynamicTexts() {
function downloadImage(item) {
const a = document.createElement('a');
a.href = URL.createObjectURL(item.processedBlob);
a.download = `unwatermarked_${item.name.replace(/\.[^.]+$/, '')}.png`;
a.href = item.processedUrl;
const ext = item.isPdf ? 'pdf' : 'png';
// Filename rule: original_cleaned.ext
const originalBase = item.name.replace(/\.[^.]+$/, '');
a.download = `${originalBase}_cleaned.${ext}`;
a.click();
}
@ -277,25 +422,75 @@ async function downloadAll() {
const zip = new JSZip();
completed.forEach(item => {
const filename = `unwatermarked_${item.name.replace(/\.[^.]+$/, '')}.png`;
const ext = item.isPdf ? 'pdf' : 'png';
const originalBase = item.name.replace(/\.[^.]+$/, '');
const filename = `${originalBase}_cleaned.${ext}`;
zip.file(filename, item.processedBlob);
});
const blob = await zip.generateAsync({ type: 'blob' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `unwatermarked_${Date.now()}.zip`;
a.download = `cleaned_files_${Date.now()}.zip`;
a.click();
}
function showLoading(text = null) {
loadingOverlay.style.display = 'flex';
const textEl = loadingOverlay.querySelector('p');
if (textEl && text) textEl.textContent = text;
async function processPdf(file, onProgress) {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument(arrayBuffer).promise;
const total = pdf.numPages;
let doc = null;
let previewDataUrl = null;
for (let i = 1; i <= total; i++) {
if (onProgress) onProgress(i, total);
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2 });
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
// For PDF, we use Simple Clone (NotebookLM config) as fallback,
// because we don't know if it's Gemini or NotebookLM strictly from just being a PDF.
// But engine has `removeWatermarkFromImage` which checks dimensions.
// So we just call that. If it's Gemini dimensions, it uses Alpha Map.
// If it's NotebookLM dimensions, it uses NotebookLM Alpha Map.
// If it's neither? Gemini 48px Alpha Map (default).
// This might be wrong for generic slides.
// But consistent with "Original Code" behavior.
// However, for NotebookLM PDFs, I previously used `removeWatermarkSimpleClone` with `NOTEBOOK_CLONE_CONFIG`.
// If I use `engine.removeWatermarkFromImage`, it might fail if page size doesn't match exactly.
// I will use `forceSimpleClone: true` for PDF pages to ensure cleaning, as PDFs are often resampled/sized differently.
// Wait, `removeWatermarkFromImage` supports `forceSimpleClone` option which uses `NOTEBOOK_CLONE_CONFIG` if type is notebooklm OR default.
// In `detectWatermarkConfig`, if not NotebookLM, it defaults to Gemini.
// If I force clone on Gemini type, it uses GEMINI_CLONE_CONFIG? No, I didn't add GEMINI_CLONE_CONFIG in the *restored* engine.
// I only added `NOTEBOOK_CLONE_CONFIG`.
// So I should force clone for PDF to use `NOTEBOOK_CLONE_CONFIG`?
// Let's assume PDF pages are NotebookLM slides.
await engine.removeWatermarkFromImage(canvas, { forceSimpleClone: true });
const imgData = canvas.toDataURL('image/jpeg', 0.9);
// Capture first page as preview
if (i === 1) {
previewDataUrl = imgData;
doc = new jsPDF({
orientation: viewport.width > viewport.height ? 'l' : 'p',
unit: 'px',
format: [viewport.width, viewport.height]
});
} else {
doc.addPage([viewport.width, viewport.height], viewport.width > viewport.height ? 'l' : 'p');
}
doc.addImage(imgData, 'JPEG', 0, 0, viewport.width, viewport.height);
}
return { pdfBlob: doc.output('blob'), previewDataUrl };
}
function hideLoading() {
loadingOverlay.style.display = 'none';
}
init();
init();

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -7,25 +7,68 @@ import { calculateAlphaMap } from './alphaMap.js';
import { removeWatermark } from './blendModes.js';
import BG_48_PATH from '../assets/bg_48.png';
import BG_96_PATH from '../assets/bg_96.png';
import BG_NOTEBOOK_PATH from '../assets/bg_notebook.png';
import BG_NOTEBOOK_LARGE_PATH from '../assets/bg_notebook_large.png';
const NOTEBOOK_CLONE_CONFIG = {
wRatio: 0.0825, wRef: 'long',
hRatio: 0.0375, hRef: 'short',
mrRatio: 0.0025, mrRef: 'long',
mbRatio: 0.0027, mbRef: 'short',
featherSize: 12
};
/**
* Check if image is from NotebookLM based on exact dimensions
*/
function isNotebookLM(width, height) {
return (width === 571 && height === 1024) ||
(width === 1536 && height === 2752) ||
(width === 2752 && height === 1536);
}
/**
* Detect watermark configuration based on image size
* @param {number} imageWidth - Image width
* @param {number} imageHeight - Image height
* @returns {Object} Watermark configuration {logoSize, marginRight, marginBottom}
* @returns {Object} Watermark configuration
*/
export function detectWatermarkConfig(imageWidth, imageHeight) {
// Gemini's watermark rules:
// NotebookLM detection
if (isNotebookLM(imageWidth, imageHeight)) {
if (imageWidth === 571 && imageHeight === 1024) {
return {
type: 'notebooklm',
logoWidth: 72,
logoHeight: 9,
marginRight: 3,
marginBottom: 3
};
} else {
// Large NotebookLM
return {
type: 'notebooklm',
logoWidth: 190,
logoHeight: 20,
marginRight: 11,
marginBottom: 10
};
}
}
// Gemini's watermark rules (unchanged):
// If both image width and height are greater than 1024, use 96×96 watermark
// Otherwise, use 48×48 watermark
if (imageWidth > 1024 && imageHeight > 1024) {
return {
type: 'gemini',
logoSize: 96,
marginRight: 64,
marginBottom: 64
};
} else {
return {
type: 'gemini',
logoSize: 48,
marginRight: 32,
marginBottom: 32
@ -37,17 +80,19 @@ export function detectWatermarkConfig(imageWidth, imageHeight) {
* Calculate watermark position in image based on image size and watermark configuration
* @param {number} imageWidth - Image width
* @param {number} imageHeight - Image height
* @param {Object} config - Watermark configuration {logoSize, marginRight, marginBottom}
* @param {Object} config - Watermark configuration
* @returns {Object} Watermark position {x, y, width, height}
*/
export function calculateWatermarkPosition(imageWidth, imageHeight, config) {
const { logoSize, marginRight, marginBottom } = config;
const logoWidth = config.logoWidth || config.logoSize;
const logoHeight = config.logoHeight || config.logoSize;
const { marginRight, marginBottom } = config;
return {
x: imageWidth - marginRight - logoSize,
y: imageHeight - marginBottom - logoSize,
width: logoSize,
height: logoSize
x: imageWidth - marginRight - logoWidth,
y: imageHeight - marginBottom - logoHeight,
width: logoWidth,
height: logoHeight
};
}
@ -64,6 +109,8 @@ export class WatermarkEngine {
static async create() {
const bg48 = new Image();
const bg96 = new Image();
const bgNotebook = new Image();
const bgNotebookLarge = new Image();
await Promise.all([
new Promise((resolve, reject) => {
@ -75,10 +122,20 @@ export class WatermarkEngine {
bg96.onload = resolve;
bg96.onerror = reject;
bg96.src = BG_96_PATH;
}),
new Promise((resolve, reject) => {
bgNotebook.onload = resolve;
bgNotebook.onerror = reject;
bgNotebook.src = BG_NOTEBOOK_PATH;
}),
new Promise((resolve, reject) => {
bgNotebookLarge.onload = resolve;
bgNotebookLarge.onerror = reject;
bgNotebookLarge.src = BG_NOTEBOOK_LARGE_PATH;
})
]);
return new WatermarkEngine({ bg48, bg96 });
return new WatermarkEngine({ bg48, bg96, bgNotebook, bgNotebookLarge });
}
/**
@ -114,11 +171,119 @@ export class WatermarkEngine {
}
/**
* Remove watermark from image based on watermark size
* Remove NotebookLM watermark using alpha map and background estimation
*/
removeNotebookLMWatermark(imageData, position, isLarge) {
const { x, y, width, height } = position;
const imgWidth = imageData.width;
// Get alpha map
const bgImage = isLarge ? this.bgCaptures.bgNotebookLarge : this.bgCaptures.bgNotebook;
// Create canvas to extract alpha map
const alphaCanvas = document.createElement('canvas');
alphaCanvas.width = width;
alphaCanvas.height = height;
const alphaCtx = alphaCanvas.getContext('2d');
alphaCtx.drawImage(bgImage, 0, 0, width, height);
const alphaData = alphaCtx.getImageData(0, 0, width, height);
// Calculate background colors from pixels above the watermark
const bgColors = [];
for (let col = 0; col < width; col++) {
let sumR = 0, sumG = 0, sumB = 0, count = 0;
for (let dy = -25; dy < -5; dy++) {
const sampleY = y + dy;
if (sampleY >= 0) {
const idx = (sampleY * imgWidth + (x + col)) * 4;
sumR += imageData.data[idx];
sumG += imageData.data[idx + 1];
sumB += imageData.data[idx + 2];
count++;
}
}
bgColors.push({
r: count > 0 ? sumR / count : 0,
g: count > 0 ? sumG / count : 0,
b: count > 0 ? sumB / count : 0
});
}
// Process each pixel in the watermark region
// Use background replacement for all watermark pixels with alpha blending at edges
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
const alphaIdx = (row * width + col) * 4;
const alpha = alphaData.data[alphaIdx] / 255.0;
if (alpha < 0.02) continue; // Skip nearly transparent pixels
const imgIdx = ((y + row) * imgWidth + (x + col)) * 4;
const bg = bgColors[col];
// Blend current pixel with background based on alpha
const blendFactor = Math.min(alpha * 2.0, 1.0);
for (let c = 0; c < 3; c++) {
const current = imageData.data[imgIdx + c];
const bgVal = c === 0 ? bg.r : (c === 1 ? bg.g : bg.b);
const blended = current * (1 - blendFactor) + bgVal * blendFactor;
imageData.data[imgIdx + c] = Math.round(blended);
}
}
}
}
/**
* Remove watermark using simple cloning technique (SlideClean approach)
* Robust for NotebookLM PDFs and images with varying sizes
*/
removeWatermarkSimpleClone(ctx, width, height, config = NOTEBOOK_CLONE_CONFIG) {
const longEdge = Math.max(width, height);
const shortEdge = Math.min(width, height);
const getDim = (ratio, ref) => Math.round((ref === 'short' ? shortEdge : longEdge) * ratio);
const wmW = getDim(config.wRatio, config.wRef);
const wmH = getDim(config.hRatio, config.hRef);
const mr = getDim(config.mrRatio, config.mrRef);
const mb = getDim(config.mbRatio, config.mbRef);
const feather = config.featherSize || 12;
const x = width - wmW - mr;
const y = height - wmH - mb;
const srcY = Math.max(0, y - wmH);
if (srcY < 0) return;
const src = ctx.getImageData(x, srcY, wmW, wmH);
const dst = ctx.getImageData(x, y, wmW, wmH);
const res = ctx.createImageData(wmW, wmH);
for (let i = 0; i < wmH; i++) {
for (let j = 0; j < wmW; j++) {
const idx = (i * wmW + j) * 4;
let a = 1.0;
// Simple feathering at edges
if (i < feather) a = Math.min(a, i / feather);
if (j < feather) a = Math.min(a, j / feather);
for (let c = 0; c < 4; c++) {
res.data[idx + c] = Math.round(dst.data[idx + c] * (1 - a) + src.data[idx + c] * a);
}
}
}
ctx.putImageData(res, x, y);
}
/**
* Remove watermark from image
* @param {HTMLImageElement|HTMLCanvasElement} image - Input image
* @param {Object} [options] - Options
* @param {boolean} [options.forceSimpleClone=false] - Force use of simple clone method
* @returns {Promise<HTMLCanvasElement>} Processed canvas
*/
async removeWatermarkFromImage(image) {
async removeWatermarkFromImage(image, options = {}) {
// Create canvas to process image
const canvas = document.createElement('canvas');
canvas.width = image.width;
@ -128,6 +293,12 @@ export class WatermarkEngine {
// Draw original image onto canvas
ctx.drawImage(image, 0, 0);
// Use simple clone if forced (e.g. for PDFs or explicit override)
if (options.forceSimpleClone) {
this.removeWatermarkSimpleClone(ctx, canvas.width, canvas.height, NOTEBOOK_CLONE_CONFIG);
return canvas;
}
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
@ -135,11 +306,15 @@ export class WatermarkEngine {
const config = detectWatermarkConfig(canvas.width, canvas.height);
const position = calculateWatermarkPosition(canvas.width, canvas.height, config);
// Get alpha map for watermark size
const alphaMap = await this.getAlphaMap(config.logoSize);
// Remove watermark from image data
removeWatermark(imageData, alphaMap, position);
if (config.type === 'notebooklm') {
// Use alpha map + background estimation for NotebookLM
const isLarge = canvas.width > 1000 || canvas.height > 1000;
this.removeNotebookLMWatermark(imageData, position, isLarge);
} else {
// Original Gemini logic - unchanged, using static assets for 48/96
const alphaMap = await this.getAlphaMap(config.logoSize);
removeWatermark(imageData, alphaMap, position);
}
// Write processed image data back to canvas
ctx.putImageData(imageData, 0, 0);
@ -158,7 +333,10 @@ export class WatermarkEngine {
const position = calculateWatermarkPosition(imageWidth, imageHeight, config);
return {
size: config.logoSize,
size: config.logoSize || config.logoWidth,
width: config.logoWidth || config.logoSize,
height: config.logoHeight || config.logoSize,
type: config.type,
position: position,
config: config
};

View File

@ -9,14 +9,18 @@ const i18n = {
},
async loadTranslations(locale) {
const res = await fetch(`/i18n/${locale}.json?_=${Date.now()}`);
const res = await fetch(`./i18n/${locale}.json?_=${Date.now()}`);
this.translations = await res.json();
this.locale = locale;
localStorage.setItem('locale', locale);
},
t(key) {
return this.translations[key] || key;
let text = this.translations[key] || key;
if (typeof text === 'string') {
text = text.replace('{{year}}', new Date().getFullYear());
}
return text;
},
applyTranslations() {

View File

@ -27,8 +27,9 @@
"footer.desc": "Gemini Watermark Remover, for educational purposes only",
"footer.links": "Links",
"footer.terms": "Terms of Use",
"footer.github": "GitHub",
"footer.tech": "Technology",
"footer.copyright": 2025 Gemini Watermark Remover. All rights reserved.",
"footer.copyright": {{year}} Gemini Watermark Remover. All rights reserved.",
"loading.text": "Processing...",
"status.loading": "Loading resources...",
"status.pending": "Pending...",
@ -39,5 +40,8 @@
"info.watermark": "Detected Watermark",
"info.position": "Position",
"info.status": "Status",
"info.removed": "Watermark Removed"
"info.removed": "Watermark Removed",
"original.not_gemini": "⚠️ This image may not be made with Gemini. Lossless processing unavailable.",
"original.not_original": "⚠️ This image may not be original size. Lossless processing unavailable.",
"original.pass": "✅ This image made with Gemini. Lossless processing supported."
}

View File

@ -27,8 +27,9 @@
"footer.desc": "Gemini 无损去水印工具,本工具仅供学习交流使用",
"footer.links": "链接",
"footer.terms": "使用条款",
"footer.github": "GitHub",
"footer.tech": "技术",
"footer.copyright": 2025 Gemini Watermark Remover. All rights reserved.",
"footer.copyright": {{year}} Gemini Watermark Remover. All rights reserved.",
"loading.text": "正在处理...",
"status.loading": "正在加载资源...",
"status.pending": "等待处理...",
@ -39,5 +40,8 @@
"info.watermark": "检测到的水印",
"info.position": "位置",
"info.status": "状态",
"info.removed": "水印已移除"
"info.removed": "水印已移除",
"original.not_gemini": "⚠️ 此图片非 Gemini 生成的原始图片,可能无法进行无损去水印",
"original.not_original": "⚠️ 此图片非原始尺寸,可能无法进行无损去水印",
"original.pass": "✅ 此图片为 Gemini 生成的原始图片,可进行无损去水印"
}

View File

@ -21,7 +21,7 @@ const loadImage = (src) => new Promise((resolve, reject) => {
const canvasToBlob = (canvas, type = 'image/png') =>
new Promise(resolve => canvas.toBlob(resolve, type));
const isValidGeminiImage = (img) => img.closest('model-response,.generated-image-container') !== null;
const isValidGeminiImage = (img) => img.closest('generated-image,.generated-image-container') !== null;
const findGeminiImages = () =>
[...document.querySelectorAll('img[src*="googleusercontent.com"]')].filter(isValidGeminiImage);

54
src/utils.js 100644
View File

@ -0,0 +1,54 @@
import exifr from 'exifr';
import i18n from './i18n.js';
export function loadImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = e.target.result;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
export async function checkOriginal(file) {
try {
const exif = await exifr.parse(file, { xmp: true });
return {
is_google: exif?.Credit === 'Made with Google AI',
is_original: ['ImageWidth', 'ImageHeight'].every(key => exif?.[key])
}
} catch {
return { is_google: false, is_original: false };
}
}
export function getOriginalStatus({ is_google, is_original }) {
if (!is_google) return i18n.t('original.not_gemini');
if (!is_original) return i18n.t('original.not_original');
return '';
}
const statusMessage = document.getElementById('statusMessage');
export function setStatusMessage(message = '', type = '') {
statusMessage.textContent = message;
statusMessage.style.display = message ? 'block' : 'none';
const colorMap = { warn: 'text-warn', success: 'text-success' };
statusMessage.classList.remove(...Object.values(colorMap));
if (colorMap[type]) statusMessage.classList.add(colorMap[type]);
}
const loadingOverlay = document.getElementById('loadingOverlay');
export function showLoading(text = null) {
loadingOverlay.style.display = 'flex';
const textEl = loadingOverlay.querySelector('p');
if (textEl && text) textEl.textContent = text;
}
export function hideLoading() {
loadingOverlay.style.display = 'none';
}

5
wrangler.toml 100644
View File

@ -0,0 +1,5 @@
name = "gemini-watermark-remover"
compatibility_date = "2025-01-16"
[assets]
directory = "./dist"

1015
yarn.lock 100644

File diff suppressed because it is too large Load Diff