init commit
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Jad
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
[中文文档](README_zh.md)
|
||||
|
||||
# Gemini Lossless Watermark Remover - [banana.ovo.re](https://banana.ovo.re)
|
||||
|
||||
A high-performance, 100% client-side tool for removing Gemini AI watermarks. Built with pure JavaScript, it leverages a mathematically precise **Reverse Alpha Blending** algorithm rather than unpredictable AI inpainting.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://count.getloli.com/@gemini-watermark-remover?name=gemini-watermark-remover&theme=minecraft&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=auto" width="400">
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **100% Client-side** - No backend, no server-side processing. Your data stays in your browser.
|
||||
- ✅ **Privacy-First** - Images are never uploaded to any server. Period.
|
||||
- ✅ **Mathematical Precision** - Based on the Reverse Alpha Blending formula, not "hallucinating" AI models.
|
||||
- ✅ **Auto-Detection** - Intelligent recognition of 48×48 or 96×96 watermark variants.
|
||||
- ✅ **User Friendly** - Simple drag-and-drop interface with instant processing.
|
||||
- ✅ **Cross-Platform** - Runs smoothly on all modern web browsers.
|
||||
|
||||
## Examples
|
||||
|
||||
<details open>
|
||||
<summary>Click to Expand/Collapse Examples</summary>
|
||||
|
||||
|
||||
| Original Image | Watermark Removed |
|
||||
| :---: | :----: |
|
||||
| <img src="docs/1.webp" width="400"> | <img src="docs/unwatermarked_1.webp" width="400"> |
|
||||
| <img src="docs/2.webp" width="400"> | <img src="docs/unwatermarked_2.webp" width="400"> |
|
||||
| <img src="docs/3.webp" width="400"> | <img src="docs/unwatermarked_3.webp" width="400"> |
|
||||
| <img src="docs/4.webp" width="400"> | <img src="docs/unwatermarked_4.webp" width="400"> |
|
||||
| <img src="docs/5.webp" width="400"> | <img src="docs/unwatermarked_5.webp" width="400"> |
|
||||
|
||||
</details>
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
> **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
|
||||
> - Corrupted or unusual image formats
|
||||
> - Edge cases not covered by testing
|
||||
>
|
||||
> 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.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Open `index.html` in your browser (or visit the hosted link).
|
||||
2. Drag and drop or click to select your Gemini-generated image.
|
||||
3. The engine will automatically process and remove the watermark.
|
||||
4. Download the cleaned image.
|
||||
|
||||
## How it Works
|
||||
|
||||
### The Gemini Watermarking Process
|
||||
|
||||
Gemini applies watermarks using standard alpha compositing:
|
||||
|
||||
$$watermarked = \alpha \cdot logo + (1 - \alpha) \cdot original$$
|
||||
|
||||
Where:
|
||||
- `watermarked`: The pixel value with the watermark.
|
||||
- `α`: The Alpha channel value (0.0 - 1.0).
|
||||
- `logo`: The watermark logo color value (White = 255).
|
||||
- `original`: The raw, original pixel value we want to recover.
|
||||
|
||||
### The Reverse Solution
|
||||
|
||||
To remove the watermark, we solve for `original`:
|
||||
|
||||
$$original = \frac{watermarked - \alpha \cdot logo}{1 - \alpha}$$
|
||||
|
||||
By capturing the watermark on a known solid background, we reconstruct the exact Alpha map and apply the inverse formula to restore the original pixels with zero loss.
|
||||
|
||||
## Detection Rules
|
||||
|
||||
| Image Dimension Condition | Watermark Size | Right Margin | Bottom Margin |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| Width > 1024 **AND** Height > 1024 | 96×96 | 64px | 64px |
|
||||
| Otherwise | 48×48 | 32px | 32px |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
gemini-watermark-web/
|
||||
├── index.html # Main entry point
|
||||
├── css/
|
||||
│ └── style.css # UI Styling
|
||||
├── js/
|
||||
│ ├── core/
|
||||
│ │ ├── alphaMap.js # Alpha map calculation logic
|
||||
│ │ ├── blendModes.js # Implementation of Reverse Alpha Blending
|
||||
│ │ └── watermarkEngine.js # Main engine coordinator
|
||||
│ ├── assets/
|
||||
│ │ ├── bg-capture-48.png # Pre-captured 48×48 watermark map
|
||||
│ │ └── bg-capture-96.png # Pre-captured 96×96 watermark map
|
||||
│ └── app.js # UI Interaction & Event handling
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Core Modules
|
||||
|
||||
### alphaMap.js
|
||||
|
||||
Calculates the Alpha channel by comparing captured watermark assets:
|
||||
|
||||
```javascript
|
||||
export function calculateAlphaMap(bgCaptureImageData) {
|
||||
// Extract max RGB channel and normalize to [0, 1]
|
||||
const alphaMap = new Float32Array(width * height);
|
||||
for (let i = 0; i < alphaMap.length; i++) {
|
||||
const maxChannel = Math.max(r, g, b);
|
||||
alphaMap[i] = maxChannel / 255.0;
|
||||
}
|
||||
return alphaMap;
|
||||
}
|
||||
```
|
||||
|
||||
### blendModes.js
|
||||
|
||||
The mathematical core of the tool:
|
||||
|
||||
```javascript
|
||||
export function removeWatermark(imageData, alphaMap, position) {
|
||||
// Formula: original = (watermarked - α × 255) / (1 - α)
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
const alpha = Math.min(alphaMap[idx], MAX_ALPHA);
|
||||
const original = (watermarked - alpha * 255) / (1.0 - alpha);
|
||||
imageData.data[idx] = Math.max(0, Math.min(255, original));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
- ✅ Chrome 90+
|
||||
- ✅ Firefox 88+
|
||||
- ✅ Safari 14+
|
||||
- ✅ Edge 90+
|
||||
|
||||
Required APIs:
|
||||
- ES6 Modules
|
||||
- Canvas API
|
||||
- Async/Await
|
||||
- TypedArray (Float32Array, Uint8ClampedArray)
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only removes **Gemini visible watermarks** <small>(the semi-transparent logo in bottom-right)</small>
|
||||
- Does not remove invisible/steganographic watermarks. <small>[(Learn more about SynthID)](https://support.google.com/gemini/answer/16722517)</small>
|
||||
- Designed for Gemini's current watermark pattern <small>(as of 2025)</small>
|
||||
|
||||
## Legal Disclaimer
|
||||
|
||||
This tool is provided for **personal and educational use only**.
|
||||
|
||||
The removal of watermarks may have legal implications depending on your jurisdiction and the intended use of the images. Users are solely responsible for ensuring their use of this tool complies with applicable laws, terms of service, and intellectual property rights.
|
||||
|
||||
The author does not condone or encourage the misuse of this tool for copyright infringement, misrepresentation, or any other unlawful purposes.
|
||||
|
||||
**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
|
||||
|
||||
[MIT License](./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
|
||||
|
||||
This project is a JavaScript port of the [Gemini Watermark Tool](https://github.com/allenk/GeminiWatermarkTool) C++ implementation.
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
[English](README.md)
|
||||
|
||||
# Gemini 无损去水印工具 - [banana.ovo.re](https://banana.ovo.re)
|
||||
|
||||
基于 Javascript 的纯浏览器端 Gemini AI 图像无损去水印工具,使用数学精确的反向 Alpha 混合算法
|
||||
|
||||
<p align="center">
|
||||
<img src="https://count.getloli.com/@gemini-watermark-remover?name=gemini-watermark-remover&theme=minecraft&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=auto" width="400">
|
||||
</p>
|
||||
|
||||
## 特性
|
||||
|
||||
- ✅ **纯浏览器端处理** - 无需后端服务器,所有处理在本地完成
|
||||
- ✅ **隐私保护** - 图片不会上传到任何服务器
|
||||
- ✅ **数学精确** - 基于反向 Alpha 混合算法,非 AI 模型
|
||||
- ✅ **自动检测** - 自动识别 48×48 或 96×96 水印尺寸
|
||||
- ✅ **易于使用** - 拖拽选择图片,一键处理
|
||||
- ✅ **跨平台** - 支持所有现代浏览器
|
||||
|
||||
## 效果示例
|
||||
|
||||
<details open>
|
||||
<summary>点击查看/收起示例</summary>
|
||||
|
||||
|
||||
| 原图 | 去水印后 |
|
||||
| :---: | :----: |
|
||||
| <img src="docs/1.webp" width="400"> | <img src="docs/unwatermarked_1.webp" width="400"> |
|
||||
| <img src="docs/2.webp" width="400"> | <img src="docs/unwatermarked_2.webp" width="400"> |
|
||||
| <img src="docs/3.webp" width="400"> | <img src="docs/unwatermarked_3.webp" width="400"> |
|
||||
| <img src="docs/4.webp" width="400"> | <img src="docs/unwatermarked_4.webp" width="400"> |
|
||||
| <img src="docs/5.webp" width="400"> | <img src="docs/unwatermarked_5.webp" width="400"> |
|
||||
|
||||
</details>
|
||||
|
||||
## ⚠️ 使用需注意
|
||||
|
||||
> **使用此工具产生的风险由用户自行承担**
|
||||
>
|
||||
> 本工具涉及对图像数据的修改。尽管在设计上力求处理结果的可靠性,但由于以下因素,仍可能产生非预期的处理结果:
|
||||
> - Gemini 水印实现方式的更新或变动
|
||||
> - 图像文件损坏或使用了非标准格式
|
||||
> - 测试案例未能覆盖的边界情况
|
||||
>
|
||||
> 作者对任何形式的数据丢失、图像损坏或非预期的修改结果不承担法律责任。使用本工具即代表您已了解并接受上述风险。
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 在浏览器中打开 `index.html`
|
||||
2. 拖拽或点击选择带水印的 Gemini 图片
|
||||
3. 图片会自动开始处理,移除水印
|
||||
4. 下载处理后的图片
|
||||
|
||||
## 算法原理
|
||||
|
||||
### Gemini 添加水印的方式
|
||||
|
||||
Gemini 通过以下方式添加水印:
|
||||
|
||||
$$watermarked = \alpha \cdot logo + (1 - \alpha) \cdot original$$
|
||||
|
||||
其中:
|
||||
- `watermarked`: 带水印的像素值
|
||||
- `α`: Alpha 通道值 (0.0-1.0)
|
||||
- `logo`: 水印 logo 的颜色值(白色 = 255)
|
||||
- `original`: 原始像素值
|
||||
|
||||
### 反向求解移除水印
|
||||
|
||||
为了去除水印,可以反向求解如下:
|
||||
|
||||
$$original = \frac{watermarked - \alpha \cdot logo}{1 - \alpha}$$
|
||||
|
||||
通过在纯色背景上捕获水印,我们可以重建 Alpha 通道,然后应用反向公式恢复原始图像
|
||||
|
||||
## 水印检测规则
|
||||
|
||||
| 图像尺寸条件 | 水印尺寸 | 右边距 | 下边距 |
|
||||
|------------|---------|--------|--------|
|
||||
| 宽 > 1024 **且** 高 > 1024 | 96×96 | 64px | 64px |
|
||||
| 其他情况 | 48×48 | 32px | 32px |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
gemini-watermark-web/
|
||||
├── index.html # 主页面
|
||||
├── css/
|
||||
│ └── style.css # 样式文件
|
||||
├── js/
|
||||
│ ├── core/
|
||||
│ │ ├── alphaMap.js # Alpha map 计算
|
||||
│ │ ├── blendModes.js # 反向 alpha 混合算法
|
||||
│ │ └── watermarkEngine.js # 主引擎
|
||||
│ ├── assets/
|
||||
│ │ ├── bg-capture-48.png # 48×48 水印背景
|
||||
│ │ └── bg-capture-96.png # 96×96 水印背景
|
||||
│ └── app.js # UI 交互逻辑
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 核心模块
|
||||
|
||||
### alphaMap.js
|
||||
|
||||
从背景捕获图像计算 Alpha 通道:
|
||||
|
||||
```javascript
|
||||
export function calculateAlphaMap(bgCaptureImageData) {
|
||||
// 提取 RGB 通道最大值并归一化到 [0, 1]
|
||||
const alphaMap = new Float32Array(width * height);
|
||||
for (let i = 0; i < alphaMap.length; i++) {
|
||||
const maxChannel = Math.max(r, g, b);
|
||||
alphaMap[i] = maxChannel / 255.0;
|
||||
}
|
||||
return alphaMap;
|
||||
}
|
||||
```
|
||||
|
||||
### blendModes.js
|
||||
|
||||
实现反向 Alpha 混合算法:
|
||||
|
||||
```javascript
|
||||
export function removeWatermark(imageData, alphaMap, position) {
|
||||
// 对每个像素应用公式:original = (watermarked - α × 255) / (1 - α)
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
const alpha = Math.min(alphaMap[idx], MAX_ALPHA);
|
||||
const original = (watermarked - alpha * 255) / (1.0 - alpha);
|
||||
imageData.data[idx] = Math.max(0, Math.min(255, original));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### watermarkEngine.js
|
||||
|
||||
主引擎类,协调整个处理流程:
|
||||
|
||||
```javascript
|
||||
export class WatermarkEngine {
|
||||
async removeWatermarkFromImage(image) {
|
||||
// 1. 检测水印尺寸
|
||||
const config = detectWatermarkConfig(width, height);
|
||||
|
||||
// 2. 获取 alpha map
|
||||
const alphaMap = await this.getAlphaMap(config.logoSize);
|
||||
|
||||
// 3. 移除水印
|
||||
removeWatermark(imageData, alphaMap, position);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 浏览器兼容性
|
||||
|
||||
- ✅ Chrome 90+
|
||||
- ✅ Firefox 88+
|
||||
- ✅ Safari 14+
|
||||
- ✅ Edge 90+
|
||||
|
||||
需要支持:
|
||||
- ES6 Modules
|
||||
- Canvas API
|
||||
- Async/Await
|
||||
- TypedArray (Float32Array, Uint8ClampedArray)
|
||||
|
||||
---
|
||||
|
||||
## 局限性
|
||||
|
||||
- 只去除了 **Gemini 可见的水印**<small>(即右下角的半透明 Logo)</small>
|
||||
- 无法去除隐形或隐写水印。<small>[(了解更多关于 SynthID 的信息)](https://support.google.com/gemini/answer/16722517)</small>
|
||||
- 针对 Gemini 当前的水印模式设计<small>(截至 2025 年)</small>
|
||||
|
||||
## 免责声明
|
||||
|
||||
本工具仅限**个人学习研究**所用,不得用于商业用途。
|
||||
|
||||
根据您所在的司法管辖区及图像的实际用途,移除水印的行为可能具有潜在的法律影响。用户需自行确保其使用行为符合适用法律、相关服务条款以及知识产权规定,并对此承担全部责任。
|
||||
|
||||
作者不纵容也不鼓励将本工具用于侵犯版权、虚假陈述或任何其他非法用途。
|
||||
|
||||
**本软件按“原样”提供,不提供任何形式(无论是明示或暗示)的保证。在任何情况下,作者均不对因使用本软件而产生的任何索赔、损害或其他责任承担任何义务。**
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT License](./LICENSE)
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [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++ 版本移植
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 959 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 815 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 809 KiB |
|
After Width: | Height: | Size: 959 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"title": "Gemini Watermark Remover - Lossless Watermark Removal Tool",
|
||||
"header.title": "Gemini Watermark Remover",
|
||||
"nav.principle": "How It Works?",
|
||||
"main.title": "Gemini AI Watermark Removal",
|
||||
"main.subtitle": "Based on reverse alpha blending algorithm, pure browser-side processing, Free, Fast, and Lossless",
|
||||
"upload.text": "Click to select or drag images here",
|
||||
"upload.hint": "Supports JPG, PNG, WebP",
|
||||
"step.1": "Select Original Image",
|
||||
"step.2": "Auto Processing",
|
||||
"step.3": "Save Clean Image",
|
||||
"preview.original": "Original Preview",
|
||||
"preview.result": "Processed Result",
|
||||
"panel.title": "Control Panel",
|
||||
"btn.download": "Download",
|
||||
"btn.reset": "Reset / Change Image",
|
||||
"progress.text": "Progress",
|
||||
"btn.downloadAll": "Download All",
|
||||
"feature.title": "Features",
|
||||
"feature.speed.title": "Lightning Fast",
|
||||
"feature.speed.desc": "Based on modern browser technologies, millisecond response, no waiting queue",
|
||||
"feature.privacy.title": "Privacy First",
|
||||
"feature.privacy.desc": "Pure frontend processing, images never leave your device",
|
||||
"feature.free.title": "Completely Free",
|
||||
"feature.free.desc": "No usage limits, no hidden fees, ready to use",
|
||||
"footer.desc": "Gemini Watermark Remover, for educational purposes only",
|
||||
"footer.links": "Links",
|
||||
"footer.terms": "Terms of Use",
|
||||
"footer.tech": "Technology",
|
||||
"footer.copyright": "© 2025 Gemini Watermark Remover. All rights reserved.",
|
||||
"loading.text": "Processing...",
|
||||
"status.loading": "Loading resources...",
|
||||
"status.pending": "Pending...",
|
||||
"status.processing": "Processing...",
|
||||
"status.success": "Completed",
|
||||
"status.failed": "Failed",
|
||||
"info.size": "Size",
|
||||
"info.watermark": "Detected Watermark",
|
||||
"info.position": "Position",
|
||||
"info.status": "Status",
|
||||
"info.removed": "Watermark Removed"
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"title": "Gemini Watermark Remover - Gemini 无损去水印工具",
|
||||
"header.title": "Gemini Watermark Remover",
|
||||
"nav.principle": "去水印原理",
|
||||
"main.title": "Gemini AI 图像去水印",
|
||||
"main.subtitle": "基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损",
|
||||
"upload.text": "点击选择 或 拖拽图片至此",
|
||||
"upload.hint": "支持 JPG, PNG, WebP",
|
||||
"step.1": "选择原始图片",
|
||||
"step.2": "算法自动解析",
|
||||
"step.3": "保存无水印图",
|
||||
"preview.original": "原图预览",
|
||||
"preview.result": "处理结果",
|
||||
"panel.title": "操作面板",
|
||||
"btn.download": "下载结果",
|
||||
"btn.reset": "重置 / 更换图片",
|
||||
"progress.text": "处理进度",
|
||||
"btn.downloadAll": "全部下载",
|
||||
"feature.title": "功能特点",
|
||||
"feature.speed.title": "极速处理",
|
||||
"feature.speed.desc": "基于现代浏览器技术加速处理,毫秒级响应,无需等待排队",
|
||||
"feature.privacy.title": "隐私安全",
|
||||
"feature.privacy.desc": "纯前端运行,图片数据不离机,绝不上传服务器",
|
||||
"feature.free.title": "完全免费",
|
||||
"feature.free.desc": "无次数限制,无隐藏付费,即开即用",
|
||||
"footer.desc": "Gemini 无损去水印工具,本工具仅供学习交流使用",
|
||||
"footer.links": "链接",
|
||||
"footer.terms": "使用条款",
|
||||
"footer.tech": "技术",
|
||||
"footer.copyright": "© 2025 Gemini Watermark Remover. All rights reserved.",
|
||||
"loading.text": "正在处理...",
|
||||
"status.loading": "正在加载资源...",
|
||||
"status.pending": "等待处理...",
|
||||
"status.processing": "处理中...",
|
||||
"status.success": "处理完成",
|
||||
"status.failed": "处理失败",
|
||||
"info.size": "尺寸",
|
||||
"info.watermark": "检测到的水印",
|
||||
"info.position": "位置",
|
||||
"info.status": "状态",
|
||||
"info.removed": "水印已移除"
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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" />
|
||||
<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 src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#10B981', // 对应参考图的绿色
|
||||
'primary-hover': '#059669',
|
||||
dark: '#1F2937',
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 4px 20px -2px rgba(16, 185, 129, 0.1)',
|
||||
'card': '0 0 20px rgba(0,0,0,0.05)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&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 {
|
||||
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; /* gray-100 */
|
||||
z-index: 10;
|
||||
}
|
||||
@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%);
|
||||
}
|
||||
}
|
||||
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">
|
||||
|
||||
<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="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>
|
||||
</div>
|
||||
</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 图像去水印
|
||||
</h2>
|
||||
<p class="text-base md:text-lg text-gray-500 max-w-2xl mx-auto mb-10" data-i18n="main.subtitle">
|
||||
基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损
|
||||
</p>
|
||||
|
||||
<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>
|
||||
</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" />
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</button>
|
||||
</div>
|
||||
<div id="imageList" class="space-y-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 md:mb-12">
|
||||
<h3 class="text-2xl font-bold text-gray-900" data-i18n="feature.title">功能特点</h3>
|
||||
</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>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<div class="border-t border-emerald-500 pt-8 text-center text-sm text-emerald-200">
|
||||
<p data-i18n="footer.copyright">© 2025 Gemini Watermark Remover. All rights reserved.</p>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/i18n.js"></script>
|
||||
<script type="module" src="js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* 主应用程序 - UI 交互逻辑
|
||||
*/
|
||||
|
||||
import { WatermarkEngine } from './core/watermarkEngine.js';
|
||||
import i18n from './i18n.js';
|
||||
|
||||
// 全局状态
|
||||
let engine = null;
|
||||
let imageQueue = [];
|
||||
let processedCount = 0;
|
||||
|
||||
// DOM 元素
|
||||
const uploadArea = document.getElementById('uploadArea');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const singlePreview = document.getElementById('singlePreview');
|
||||
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 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');
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async function init() {
|
||||
try {
|
||||
await i18n.init();
|
||||
setupLanguageSwitch();
|
||||
showLoading(i18n.t('status.loading'));
|
||||
|
||||
engine = await WatermarkEngine.create();
|
||||
|
||||
hideLoading();
|
||||
setupEventListeners();
|
||||
} catch (error) {
|
||||
hideLoading();
|
||||
console.error('初始化错误:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言切换
|
||||
*/
|
||||
function setupLanguageSwitch() {
|
||||
const btn = document.getElementById('langSwitch');
|
||||
btn.textContent = i18n.locale === 'zh-CN' ? 'EN' : '中文';
|
||||
btn.addEventListener('click', async () => {
|
||||
const newLocale = i18n.locale === 'zh-CN' ? 'en-US' : 'zh-CN';
|
||||
await i18n.switchLocale(newLocale);
|
||||
btn.textContent = newLocale === 'zh-CN' ? 'EN' : '中文';
|
||||
updateDynamicTexts();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
function setupEventListeners() {
|
||||
uploadArea.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', () => {
|
||||
uploadArea.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.classList.remove('dragover');
|
||||
handleFiles(Array.from(e.dataTransfer.files));
|
||||
});
|
||||
|
||||
downloadAllBtn.addEventListener('click', downloadAll);
|
||||
resetBtn.addEventListener('click', reset);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
singlePreview.style.display = 'none';
|
||||
multiPreview.style.display = 'none';
|
||||
imageQueue = [];
|
||||
processedCount = 0;
|
||||
fileInput.value = '';
|
||||
}
|
||||
|
||||
function handleFileSelect(e) {
|
||||
handleFiles(Array.from(e.target.files));
|
||||
}
|
||||
|
||||
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;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (validFiles.length === 0) return;
|
||||
|
||||
imageQueue = validFiles.map((file, index) => ({
|
||||
id: Date.now() + index,
|
||||
file,
|
||||
name: file.name,
|
||||
status: 'pending',
|
||||
originalImg: null,
|
||||
processedBlob: null
|
||||
}));
|
||||
|
||||
processedCount = 0;
|
||||
|
||||
if (validFiles.length === 1) {
|
||||
singlePreview.style.display = 'block';
|
||||
multiPreview.style.display = 'none';
|
||||
processSingle(imageQueue[0]);
|
||||
} else {
|
||||
singlePreview.style.display = 'none';
|
||||
multiPreview.style.display = 'block';
|
||||
imageList.innerHTML = '';
|
||||
updateProgress();
|
||||
multiPreview.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
imageQueue.forEach(item => createImageCard(item));
|
||||
processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
async function processSingle(item) {
|
||||
try {
|
||||
const img = await loadImage(item.file);
|
||||
item.originalImg = img;
|
||||
|
||||
originalCanvas.width = img.width;
|
||||
originalCanvas.height = img.height;
|
||||
originalCanvas.getContext('2d').drawImage(img, 0, 0);
|
||||
|
||||
// 显示图片信息
|
||||
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 result = await engine.removeWatermarkFromImage(img);
|
||||
const blob = await new Promise(resolve => result.toBlob(resolve, 'image/png'));
|
||||
item.processedBlob = blob;
|
||||
|
||||
processedImage.src = URL.createObjectURL(blob);
|
||||
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')}
|
||||
`;
|
||||
|
||||
processedSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
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.innerHTML = `
|
||||
<div class="flex flex-wrap h-full relative">
|
||||
<div class="w-full md:w-auto h-full flex">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
imageList.appendChild(card);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (processedCount > 0) {
|
||||
downloadAllBtn.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
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>');
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
progressText.textContent = `${i18n.t('progress.text')}: ${processedCount}/${imageQueue.length}`;
|
||||
}
|
||||
|
||||
function updateDynamicTexts() {
|
||||
if (progressText.textContent) {
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadImage(item) {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(item.processedBlob);
|
||||
a.download = `unwatermarked_${item.name.replace(/\.[^.]+$/, '')}.png`;
|
||||
a.click();
|
||||
}
|
||||
|
||||
async function downloadAll() {
|
||||
const completed = imageQueue.filter(item => item.status === 'completed');
|
||||
if (completed.length === 0) return;
|
||||
|
||||
const zip = new JSZip();
|
||||
completed.forEach(item => {
|
||||
const filename = `unwatermarked_${item.name.replace(/\.[^.]+$/, '')}.png`;
|
||||
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.click();
|
||||
}
|
||||
|
||||
function showLoading(text = null) {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
const textEl = loadingOverlay.querySelector('p');
|
||||
if (textEl && text) textEl.textContent = text;
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingOverlay.style.display = 'none';
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Alpha Map 计算模块
|
||||
* 从背景捕获图像计算 alpha 通道
|
||||
*/
|
||||
|
||||
/**
|
||||
* 从背景捕获图像计算 alpha map
|
||||
* @param {ImageData} bgCaptureImageData - 背景捕获的 ImageData 对象
|
||||
* @returns {Float32Array} Alpha map (值范围 0.0-1.0)
|
||||
*/
|
||||
export function calculateAlphaMap(bgCaptureImageData) {
|
||||
const { width, height, data } = bgCaptureImageData;
|
||||
const alphaMap = new Float32Array(width * height);
|
||||
|
||||
// 对每个像素,取 RGB 三个通道的最大值并归一化
|
||||
for (let i = 0; i < alphaMap.length; i++) {
|
||||
const idx = i * 4; // RGBA 格式,每个像素 4 个字节
|
||||
const r = data[idx];
|
||||
const g = data[idx + 1];
|
||||
const b = data[idx + 2];
|
||||
|
||||
// 取 RGB 最大值作为亮度值
|
||||
const maxChannel = Math.max(r, g, b);
|
||||
|
||||
// 归一化到 [0, 1] 范围
|
||||
alphaMap[i] = maxChannel / 255.0;
|
||||
}
|
||||
|
||||
return alphaMap;
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* 反向 Alpha 混合模块
|
||||
* 实现去除水印的核心算法
|
||||
*/
|
||||
|
||||
// 常量定义
|
||||
const ALPHA_THRESHOLD = 0.002; // 忽略极小的 alpha 值(噪声)
|
||||
const MAX_ALPHA = 0.99; // 避免除以接近零的值
|
||||
const LOGO_VALUE = 255; // 白色水印的颜色值
|
||||
|
||||
/**
|
||||
* 使用反向 alpha 混合移除水印
|
||||
*
|
||||
* 原理:
|
||||
* Gemini 添加水印: watermarked = α × logo + (1 - α) × original
|
||||
* 反向求解: original = (watermarked - α × logo) / (1 - α)
|
||||
*
|
||||
* @param {ImageData} imageData - 要处理的图像数据(会被原地修改)
|
||||
* @param {Float32Array} alphaMap - Alpha 通道数据
|
||||
* @param {Object} position - 水印位置 {x, y, width, height}
|
||||
*/
|
||||
export function removeWatermark(imageData, alphaMap, position) {
|
||||
const { x, y, width, height } = position;
|
||||
|
||||
// 遍历水印区域的每个像素
|
||||
for (let row = 0; row < height; row++) {
|
||||
for (let col = 0; col < width; col++) {
|
||||
// 计算在原图中的索引(RGBA 格式,每个像素 4 个字节)
|
||||
const imgIdx = ((y + row) * imageData.width + (x + col)) * 4;
|
||||
|
||||
// 计算在 alpha map 中的索引
|
||||
const alphaIdx = row * width + col;
|
||||
|
||||
// 获取 alpha 值
|
||||
let alpha = alphaMap[alphaIdx];
|
||||
|
||||
// 跳过极小的 alpha 值(噪声)
|
||||
if (alpha < ALPHA_THRESHOLD) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 限制 alpha 值,避免除零
|
||||
alpha = Math.min(alpha, MAX_ALPHA);
|
||||
const oneMinusAlpha = 1.0 - alpha;
|
||||
|
||||
// 对 RGB 三个通道应用反向 alpha 混合公式
|
||||
for (let c = 0; c < 3; c++) {
|
||||
const watermarked = imageData.data[imgIdx + c];
|
||||
|
||||
// 反向 alpha 混合公式
|
||||
const original = (watermarked - alpha * LOGO_VALUE) / oneMinusAlpha;
|
||||
|
||||
// 裁剪到 [0, 255] 范围
|
||||
imageData.data[imgIdx + c] = Math.max(0, Math.min(255, Math.round(original)));
|
||||
}
|
||||
|
||||
// Alpha 通道保持不变
|
||||
// imageData.data[imgIdx + 3] 不需要修改
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* 水印引擎主模块
|
||||
* 协调水印检测、alpha map 计算和去除操作
|
||||
*/
|
||||
|
||||
import { calculateAlphaMap } from './alphaMap.js';
|
||||
import { removeWatermark } from './blendModes.js';
|
||||
|
||||
const BG_48_PATH = '../assets/bg_48.png';
|
||||
const BG_96_PATH = '../assets/bg_96.png';
|
||||
|
||||
/**
|
||||
* 根据图像尺寸检测水印配置
|
||||
* @param {number} imageWidth - 图像宽度
|
||||
* @param {number} imageHeight - 图像高度
|
||||
* @returns {Object} 水印配置 {logoSize, marginRight, marginBottom}
|
||||
*/
|
||||
export function detectWatermarkConfig(imageWidth, imageHeight) {
|
||||
// Gemini 的水印规则:
|
||||
// 如果图像宽高都大于 1024,使用 96×96 水印
|
||||
// 否则使用 48×48 水印
|
||||
if (imageWidth > 1024 && imageHeight > 1024) {
|
||||
return {
|
||||
logoSize: 96,
|
||||
marginRight: 64,
|
||||
marginBottom: 64
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
logoSize: 48,
|
||||
marginRight: 32,
|
||||
marginBottom: 32
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算水印在图像中的位置
|
||||
* @param {number} imageWidth - 图像宽度
|
||||
* @param {number} imageHeight - 图像高度
|
||||
* @param {Object} config - 水印配置
|
||||
* @returns {Object} 水印位置 {x, y, width, height}
|
||||
*/
|
||||
export function calculateWatermarkPosition(imageWidth, imageHeight, config) {
|
||||
const { logoSize, marginRight, marginBottom } = config;
|
||||
|
||||
return {
|
||||
x: imageWidth - marginRight - logoSize,
|
||||
y: imageHeight - marginBottom - logoSize,
|
||||
width: logoSize,
|
||||
height: logoSize
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 水印引擎类
|
||||
*/
|
||||
export class WatermarkEngine {
|
||||
constructor(bgCaptures) {
|
||||
this.bgCaptures = bgCaptures;
|
||||
this.alphaMaps = {};
|
||||
}
|
||||
|
||||
static async create() {
|
||||
const bg48 = new Image();
|
||||
const bg96 = new Image();
|
||||
|
||||
await Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
bg48.onload = resolve;
|
||||
bg48.onerror = reject;
|
||||
bg48.src = BG_48_PATH;
|
||||
}),
|
||||
new Promise((resolve, reject) => {
|
||||
bg96.onload = resolve;
|
||||
bg96.onerror = reject;
|
||||
bg96.src = BG_96_PATH;
|
||||
})
|
||||
]);
|
||||
|
||||
return new WatermarkEngine({ bg48, bg96 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 从背景捕获图像获取 alpha map
|
||||
* @param {number} size - 水印尺寸 (48 或 96)
|
||||
* @returns {Promise<Float32Array>} Alpha map
|
||||
*/
|
||||
async getAlphaMap(size) {
|
||||
// 如果已缓存,直接返回
|
||||
if (this.alphaMaps[size]) {
|
||||
return this.alphaMaps[size];
|
||||
}
|
||||
|
||||
// 选择对应尺寸的背景捕获
|
||||
const bgImage = size === 48 ? this.bgCaptures.bg48 : this.bgCaptures.bg96;
|
||||
|
||||
// 创建临时 canvas 来提取 ImageData
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(bgImage, 0, 0);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
|
||||
// 计算 alpha map
|
||||
const alphaMap = calculateAlphaMap(imageData);
|
||||
|
||||
// 缓存结果
|
||||
this.alphaMaps[size] = alphaMap;
|
||||
|
||||
return alphaMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除图像上的水印
|
||||
* @param {HTMLImageElement|HTMLCanvasElement} image - 输入图像
|
||||
* @returns {Promise<HTMLCanvasElement>} 处理后的 canvas
|
||||
*/
|
||||
async removeWatermarkFromImage(image) {
|
||||
// 创建 canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 绘制原图
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
// 获取图像数据
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 检测水印配置
|
||||
const config = detectWatermarkConfig(canvas.width, canvas.height);
|
||||
const position = calculateWatermarkPosition(canvas.width, canvas.height, config);
|
||||
|
||||
// 获取对应尺寸的 alpha map
|
||||
const alphaMap = await this.getAlphaMap(config.logoSize);
|
||||
|
||||
// 移除水印
|
||||
removeWatermark(imageData, alphaMap, position);
|
||||
|
||||
// 将处理后的数据写回 canvas
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取水印信息(用于显示)
|
||||
* @param {number} imageWidth - 图像宽度
|
||||
* @param {number} imageHeight - 图像高度
|
||||
* @returns {Object} 水印信息
|
||||
*/
|
||||
getWatermarkInfo(imageWidth, imageHeight) {
|
||||
const config = detectWatermarkConfig(imageWidth, imageHeight);
|
||||
const position = calculateWatermarkPosition(imageWidth, imageHeight, config);
|
||||
|
||||
return {
|
||||
size: config.logoSize,
|
||||
position: position,
|
||||
config: config
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
const i18n = {
|
||||
locale: localStorage.getItem('locale') || (navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'),
|
||||
translations: {},
|
||||
|
||||
async init() {
|
||||
await this.loadTranslations(this.locale);
|
||||
this.applyTranslations();
|
||||
document.body.classList.remove('loading');
|
||||
},
|
||||
|
||||
async loadTranslations(locale) {
|
||||
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;
|
||||
},
|
||||
|
||||
applyTranslations() {
|
||||
document.documentElement.lang = this.locale;
|
||||
document.title = this.t('title');
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (el.tagName === 'INPUT' && el.placeholder !== undefined) {
|
||||
el.placeholder = this.t(key);
|
||||
} else {
|
||||
el.textContent = this.t(key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async switchLocale(locale) {
|
||||
await this.loadTranslations(locale);
|
||||
this.applyTranslations();
|
||||
}
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Terms of Use - Gemini Watermark Remover</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#10B981',
|
||||
'primary-hover': '#059669',
|
||||
dark: '#1F2937',
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 4px 20px -2px rgba(16, 185, 129, 0.1)',
|
||||
'card': '0 0 20px rgba(0,0,0,0.05)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&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"; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800 antialiased selection:bg-primary selection:text-white flex flex-col min-h-screen">
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="flex-grow py-12 px-4 sm:px-6">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">Terms of Use</h2>
|
||||
<p class="text-sm text-gray-400 mt-2">Last updated: December 19, 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-card border border-amber-100 overflow-hidden mb-10">
|
||||
<div class="bg-amber-50/50 border-b border-amber-100 px-6 py-4 flex items-center gap-3">
|
||||
<svg class="w-6 h-6 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
|
||||
<h3 class="text-lg font-bold text-gray-800">Important Disclaimer</h3>
|
||||
</div>
|
||||
|
||||
<div class="p-6 md:p-8 space-y-6">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900">This tool is provided for personal and educational use.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-dashed border-gray-200 my-2"></div>
|
||||
|
||||
<p class="text-sm font-bold text-gray-500 uppercase tracking-wider pl-10">A few things to keep in mind</p>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900">Respect content policies.</p>
|
||||
<p class="text-gray-600 mt-1 text-sm">The removal of watermarks may have legal or ethical implications depending on how you use the resulting images.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<svg class="w-6 h-6 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-gray-900">This tool removes visible watermarks only.</p>
|
||||
<p class="text-gray-600 mt-1 text-sm">It does not affect any invisible/steganographic watermarks that may be embedded in the image data.</p>
|
||||
|
||||
<p class="text-orange-700 mt-1 bg-orange-50 inline-block px-2 py-1 rounded text-sm">
|
||||
<a href="https://support.google.com/gemini/answer/16722517" target="_blank" rel="noopener noreferrer">
|
||||
Learn more about SynthID
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 inline-block mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
||||
<polyline points="15 3 21 3 21 9"></polyline>
|
||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6 text-gray-600">
|
||||
<section class="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-3">Privacy Policy</h3>
|
||||
<p class="leading-relaxed text-sm">
|
||||
This service provides browser-based image processing. All processing is done locally on your device. We do not upload your images to any remote servers.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="bg-white rounded-xl p-6 shadow-sm border border-gray-100">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-3">Limitation of Liability</h3>
|
||||
<p class="leading-relaxed text-sm">
|
||||
The tool is provided "as is". The developer shall not be liable for any data loss, image corruption, or legal disputes arising from the use of this tool.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center">
|
||||
<a href="index.html" class="inline-flex items-center gap-2 px-6 py-3 bg-white border border-gray-200 text-gray-700 font-medium text-xs md:text-base rounded-xl hover:bg-gray-50 hover:border-gray-300 transition-all shadow-sm">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
|
||||
I have read and agree to the terms
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<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>© 2025 Gemini Watermark Remover. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>算法测试 - Gemini 去水印工具</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: monospace;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-result {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
canvas {
|
||||
border: 1px solid #ddd;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Gemini 去水印工具 - 算法测试</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试 1: Alpha Map 计算</h2>
|
||||
<div id="test1-result"></div>
|
||||
<canvas id="test1-canvas" width="48" height="48"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试 2: 水印检测</h2>
|
||||
<div id="test2-result"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h2>测试 3: 反向 Alpha 混合</h2>
|
||||
<div id="test3-result"></div>
|
||||
<canvas id="test3-before" width="100" height="100"></canvas>
|
||||
<canvas id="test3-after" width="100" height="100"></canvas>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { calculateAlphaMap } from './js/core/alphaMap.js';
|
||||
import { removeWatermark } from './js/core/blendModes.js';
|
||||
import { detectWatermarkConfig, calculateWatermarkPosition } from './js/core/watermarkEngine.js';
|
||||
|
||||
// 测试 1: Alpha Map 计算
|
||||
function test1() {
|
||||
const canvas = document.getElementById('test1-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 创建一个渐变测试图像
|
||||
const gradient = ctx.createLinearGradient(0, 0, 48, 48);
|
||||
gradient.addColorStop(0, 'black');
|
||||
gradient.addColorStop(1, 'white');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, 48, 48);
|
||||
|
||||
// 获取 ImageData
|
||||
const imageData = ctx.getImageData(0, 0, 48, 48);
|
||||
|
||||
// 计算 alpha map
|
||||
const alphaMap = calculateAlphaMap(imageData);
|
||||
|
||||
// 验证结果
|
||||
const result = document.getElementById('test1-result');
|
||||
if (alphaMap instanceof Float32Array && alphaMap.length === 48 * 48) {
|
||||
result.className = 'test-result success';
|
||||
result.innerHTML = `
|
||||
✓ Alpha map 计算成功<br>
|
||||
- 类型: Float32Array<br>
|
||||
- 长度: ${alphaMap.length}<br>
|
||||
- 最小值: ${Math.min(...alphaMap).toFixed(3)}<br>
|
||||
- 最大值: ${Math.max(...alphaMap).toFixed(3)}
|
||||
`;
|
||||
} else {
|
||||
result.className = 'test-result error';
|
||||
result.textContent = '✗ Alpha map 计算失败';
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 2: 水印检测
|
||||
function test2() {
|
||||
const result = document.getElementById('test2-result');
|
||||
let html = '';
|
||||
|
||||
// 测试不同尺寸
|
||||
const testCases = [
|
||||
{ width: 512, height: 512, expected: 48 },
|
||||
{ width: 1024, height: 1024, expected: 48 },
|
||||
{ width: 2048, height: 2048, expected: 96 },
|
||||
{ width: 1920, height: 1080, expected: 48 },
|
||||
{ width: 3840, height: 2160, expected: 96 }
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
testCases.forEach(test => {
|
||||
const config = detectWatermarkConfig(test.width, test.height);
|
||||
const position = calculateWatermarkPosition(test.width, test.height, config);
|
||||
const passed = config.logoSize === test.expected;
|
||||
allPassed = allPassed && passed;
|
||||
|
||||
html += `
|
||||
<div style="margin: 5px 0;">
|
||||
${passed ? '✓' : '✗'} ${test.width}×${test.height}:
|
||||
检测到 ${config.logoSize}×${config.logoSize}
|
||||
(期望 ${test.expected}×${test.expected})
|
||||
位置: (${position.x}, ${position.y})
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
result.className = `test-result ${allPassed ? 'success' : 'error'}`;
|
||||
result.innerHTML = html;
|
||||
}
|
||||
|
||||
// 测试 3: 反向 Alpha 混合
|
||||
function test3() {
|
||||
const canvasBefore = document.getElementById('test3-before');
|
||||
const canvasAfter = document.getElementById('test3-after');
|
||||
const ctxBefore = canvasBefore.getContext('2d');
|
||||
const ctxAfter = canvasAfter.getContext('2d');
|
||||
|
||||
// 创建一个简单的测试图像(红色背景)
|
||||
ctxBefore.fillStyle = 'red';
|
||||
ctxBefore.fillRect(0, 0, 100, 100);
|
||||
|
||||
// 模拟添加白色水印(alpha = 0.5)
|
||||
ctxBefore.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctxBefore.fillRect(25, 25, 50, 50);
|
||||
|
||||
// 获取带水印的图像数据
|
||||
const imageData = ctxBefore.getImageData(0, 0, 100, 100);
|
||||
|
||||
// 创建一个简单的 alpha map(中心区域 alpha = 0.5)
|
||||
const alphaMap = new Float32Array(50 * 50);
|
||||
alphaMap.fill(0.5);
|
||||
|
||||
// 应用反向 alpha 混合
|
||||
const position = { x: 25, y: 25, width: 50, height: 50 };
|
||||
removeWatermark(imageData, alphaMap, position);
|
||||
|
||||
// 显示处理后的图像
|
||||
ctxAfter.putImageData(imageData, 0, 0);
|
||||
|
||||
// 验证结果(检查中心像素是否接近红色)
|
||||
const centerPixel = imageData.data[(50 * 100 + 50) * 4];
|
||||
const result = document.getElementById('test3-result');
|
||||
|
||||
if (centerPixel > 200) { // 应该接近 255 (红色)
|
||||
result.className = 'test-result success';
|
||||
result.innerHTML = `
|
||||
✓ 反向 alpha 混合成功<br>
|
||||
- 处理前: 带白色半透明水印<br>
|
||||
- 处理后: 恢复为红色背景<br>
|
||||
- 中心像素红色值: ${centerPixel} (期望 > 200)
|
||||
`;
|
||||
} else {
|
||||
result.className = 'test-result error';
|
||||
result.innerHTML = `
|
||||
✗ 反向 alpha 混合失败<br>
|
||||
- 中心像素红色值: ${centerPixel} (期望 > 200)
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
try {
|
||||
test1();
|
||||
test2();
|
||||
test3();
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error);
|
||||
alert('测试失败: ' + error.message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||