docs: update docs

main
Jad 2025-12-20 14:11:23 +08:00
parent 23837b25ac
commit fe814b1783
6 changed files with 156 additions and 94 deletions

View File

@ -46,11 +46,37 @@ A high-performance, 100% client-side tool for removing Gemini AI watermarks. Bui
## Usage
1. Open `index.html` in your browser (or visit the hosted link).
### Online Website
1. Open [banana.ovo.re](https://banana.ovo.re).
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.
### Userscript for Gemini Conversation Pages
1. Install a userscript manager (e.g., Tampermonkey or Greasemonkey).
2. Open [gemini-watermark-remover.user.js](https://banana.ovo.re/userscript/gemini-watermark-remover.user.js).
3. The script will install automatically.
4. Navigate to Gemini conversation pages.
5. Click "Copy Image" or "Download Image" to remove the watermark.
## Development
```bash
# Install dependencies
pnpm install
# Development build
pnpm dev
# Production build
pnpm build
# Local preview
pnpm serve
```
## How it Works
### The Gemini Watermarking Process
@ -83,20 +109,25 @@ By capturing the watermark on a known solid background, we reconstruct the exact
## Project Structure
```text
gemini-watermark-web/
├── index.html # Main entry point
├── css/
│ └── style.css # UI Styling
├── js/
gemini-watermark-remover/
├── public/
│ ├── index.html # Main page
│ └── terms.html # Terms of Service page
├── src/
│ ├── core/
│ │ ├── alphaMap.js # Alpha map calculation logic
│ │ ├── blendModes.js # Implementation of Reverse Alpha Blending
│ │ └── watermarkEngine.js # Main engine coordinator
│ │ └── 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
│ │ ├── bg_48.png # Pre-captured 48×48 watermark map
│ │ └── bg_96.png # Pre-captured 96×96 watermark map
│ ├── i18n/ # Internationalization language files
│ ├── userscript/ # Userscript for Gemini
│ ├── app.js # Website application entry point
│ └── i18n.js # Internationalization utilities
├── dist/ # Build output directory
├── build.js # Build script
└── package.json
```
## Core Modules

View File

@ -46,11 +46,36 @@
## 使用方法
1. 在浏览器中打开 `index.html`
### 在线使用
1. 浏览器打开 [banana.ovo.re](https://banana.ovo.re)
2. 拖拽或点击选择带水印的 Gemini 图片
3. 图片会自动开始处理,移除水印
4. 下载处理后的图片
### 油猴脚本
1. 安装油猴插件(如 Tampermonkey 或 Greasemonkey
2. 打开 [gemini-watermark-remover.user.js](https://banana.ovo.re/userscript/gemini-watermark-remover.user.js)
3. 脚本会自动安装到浏览器中
4. Gemini 对话页面点击复制或者下载图片时,会自动移除水印
## 开发
```bash
# 安装依赖
pnpm install
# 开发构建
pnpm dev
# 生产构建
pnpm build
# 本地预览
pnpm serve
```
## 算法原理
### Gemini 添加水印的方式
@ -83,20 +108,25 @@ $$original = \frac{watermarked - \alpha \cdot logo}{1 - \alpha}$$
## 项目结构
```
gemini-watermark-web/
├── index.html # 主页面
├── css/
│ └── style.css # 样式文件
├── js/
gemini-watermark-remover/
├── public/
│ ├── index.html # 主页面
│ └── terms.html # 使用条款页面
├── src/
│ ├── 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
│ │ ├── bg_48.png # 48×48 水印背景
│ │ └── bg_96.png # 96×96 水印背景
│ ├── i18n/ # 国际化语言文件
│ ├── userscript/ # 用户脚本
│ ├── app.js # 网站应用入口
│ └── i18n.js # 国际化工具
├── dist/ # 构建输出目录
├── build.js # 构建脚本
└── package.json
```
## 核心模块

View File

@ -20,7 +20,7 @@ const userscriptBanner = `// ==UserScript==
async function build() {
console.log(`Start Build... ${process.env.NODE_ENV === 'production' ? 'production' : 'development'}\r\n`);
// 构建网站 - app.js
// Build website - app.js
console.log('Building: dist/app.js');
await esbuild.build({
entryPoints: ['src/app.js'],
@ -32,7 +32,7 @@ async function build() {
minify: process.env.NODE_ENV === 'production'
});
// 构建网站 - i18n.js
// Build website - i18n.js
console.log('Building: dist/i18n.js');
await esbuild.build({
entryPoints: ['src/i18n.js'],
@ -42,7 +42,7 @@ async function build() {
minify: process.env.NODE_ENV === 'production'
});
// 构建油猴脚本
// Build userscript
console.log('Building: dist/userscript/gemini-watermark-remover.user.js');
await mkdir('dist/userscript', { recursive: true });
await esbuild.build({
@ -55,7 +55,7 @@ async function build() {
minify: false
});
// 复制静态文件
// Copy static files
console.log('Copying: src/i18n -> dist/i18n');
await cp('src/i18n', 'dist/i18n', { recursive: true });
console.log('Copying: public -> dist');

View File

@ -1,28 +1,28 @@
/**
* Alpha Map 计算模块
* 从背景捕获图像计算 alpha 通道
* Alpha Map calculator
* calculate alpha map from capture background image
*/
/**
* 从背景捕获图像计算 alpha map
* @param {ImageData} bgCaptureImageData - 背景捕获的 ImageData 对象
* @returns {Float32Array} Alpha map (值范围 0.0-1.0)
* Calculate alpha map from background captured image
* @param {ImageData} bgCaptureImageData -ImageData object for background capture
* @returns {Float32Array} Alpha map (value range 0.0-1.0)
*/
export function calculateAlphaMap(bgCaptureImageData) {
const { width, height, data } = bgCaptureImageData;
const alphaMap = new Float32Array(width * height);
// 对每个像素,取 RGB 三个通道的最大值并归一化
// For each pixel, take the maximum value of the three RGB channels and normalize it to [0, 1]
for (let i = 0; i < alphaMap.length; i++) {
const idx = i * 4; // RGBA 格式,每个像素 4 个字节
const idx = i * 4; // RGBA format, 4 bytes per pixel
const r = data[idx];
const g = data[idx + 1];
const b = data[idx + 2];
// 取 RGB 最大值作为亮度值
// Take the maximum value of the three RGB channels as the brightness value
const maxChannel = Math.max(r, g, b);
// 归一化到 [0, 1] 范围
// Normalize to [0, 1] range
alphaMap[i] = maxChannel / 255.0;
}

View File

@ -1,61 +1,61 @@
/**
* 反向 Alpha 混合模块
* 实现去除水印的核心算法
* Reverse alpha blending module
* Core algorithm for removing watermarks
*/
// 常量定义
const ALPHA_THRESHOLD = 0.002; // 忽略极小的 alpha 值(噪声)
const MAX_ALPHA = 0.99; // 避免除以接近零的值
const LOGO_VALUE = 255; // 白色水印的颜色值
// Constants definition
const ALPHA_THRESHOLD = 0.002; // Ignore very small alpha values (noise)
const MAX_ALPHA = 0.99; // Avoid division by near-zero values
const LOGO_VALUE = 255; // Color value for white watermark
/**
* 使用反向 alpha 混合移除水印
* Remove watermark using reverse alpha blending
*
* 原理
* Gemini 添加水印: watermarked = α × logo + (1 - α) × original
* 反向求解: original = (watermarked - α × logo) / (1 - α)
* Principle:
* Gemini adds watermark: watermarked = α × logo + (1 - α) × original
* Reverse solve: original = (watermarked - α × logo) / (1 - α)
*
* @param {ImageData} imageData - 要处理的图像数据会被原地修改
* @param {Float32Array} alphaMap - Alpha 通道数据
* @param {Object} position - 水印位置 {x, y, width, height}
* @param {ImageData} imageData - Image data to process (will be modified in place)
* @param {Float32Array} alphaMap - Alpha channel data
* @param {Object} position - Watermark position {x, y, width, height}
*/
export function removeWatermark(imageData, alphaMap, position) {
const { x, y, width, height } = position;
// 遍历水印区域的每个像素
// Process each pixel in the watermark area
for (let row = 0; row < height; row++) {
for (let col = 0; col < width; col++) {
// 计算在原图中的索引RGBA 格式,每个像素 4 个字节)
// Calculate index in original image (RGBA format, 4 bytes per pixel)
const imgIdx = ((y + row) * imageData.width + (x + col)) * 4;
// 计算在 alpha map 中的索引
// Calculate index in alpha map
const alphaIdx = row * width + col;
// 获取 alpha 值
// Get alpha value
let alpha = alphaMap[alphaIdx];
// 跳过极小的 alpha 值(噪声)
// Skip very small alpha values (noise)
if (alpha < ALPHA_THRESHOLD) {
continue;
}
// 限制 alpha 值,避免除零
// Limit alpha value to avoid division by near-zero
alpha = Math.min(alpha, MAX_ALPHA);
const oneMinusAlpha = 1.0 - alpha;
// 对 RGB 三个通道应用反向 alpha 混合公式
// Apply reverse alpha blending to each RGB channel
for (let c = 0; c < 3; c++) {
const watermarked = imageData.data[imgIdx + c];
// 反向 alpha 混合公式
// Reverse alpha blending formula
const original = (watermarked - alpha * LOGO_VALUE) / oneMinusAlpha;
// 裁剪到 [0, 255] 范围
// Clip to [0, 255] range
imageData.data[imgIdx + c] = Math.max(0, Math.min(255, Math.round(original)));
}
// Alpha 通道保持不变
// imageData.data[imgIdx + 3] 不需要修改
// Alpha channel remains unchanged
// imageData.data[imgIdx + 3] does not need modification
}
}
}

View File

@ -1,6 +1,6 @@
/**
* 水印引擎主模块
* 协调水印检测alpha map 计算和去除操作
* Watermark engine main module
* Coordinate watermark detection, alpha map calculation, and removal operations
*/
import { calculateAlphaMap } from './alphaMap.js';
@ -9,15 +9,15 @@ import BG_48_PATH from '../assets/bg_48.png';
import BG_96_PATH from '../assets/bg_96.png';
/**
* 根据图像尺寸检测水印配置
* @param {number} imageWidth - 图像宽度
* @param {number} imageHeight - 图像高度
* @returns {Object} 水印配置 {logoSize, marginRight, marginBottom}
* Detect watermark configuration based on image size
* @param {number} imageWidth - Image width
* @param {number} imageHeight - Image height
* @returns {Object} Watermark configuration {logoSize, marginRight, marginBottom}
*/
export function detectWatermarkConfig(imageWidth, imageHeight) {
// Gemini 的水印规则:
// 如果图像宽高都大于 1024使用 96×96 水印
// 否则使用 48×48 水印
// Gemini's watermark rules:
// 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 {
logoSize: 96,
@ -34,11 +34,11 @@ export function detectWatermarkConfig(imageWidth, imageHeight) {
}
/**
* 计算水印在图像中的位置
* @param {number} imageWidth - 图像宽度
* @param {number} imageHeight - 图像高度
* @param {Object} config - 水印配置
* @returns {Object} 水印位置 {x, y, width, height}
* 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}
* @returns {Object} Watermark position {x, y, width, height}
*/
export function calculateWatermarkPosition(imageWidth, imageHeight, config) {
const { logoSize, marginRight, marginBottom } = config;
@ -52,7 +52,8 @@ export function calculateWatermarkPosition(imageWidth, imageHeight, config) {
}
/**
* 水印引擎类
* Watermark engine class
* Coordinate watermark detection, alpha map calculation, and removal operations
*/
export class WatermarkEngine {
constructor(bgCaptures) {
@ -81,20 +82,20 @@ export class WatermarkEngine {
}
/**
* 从背景捕获图像获取 alpha map
* @param {number} size - 水印尺寸 (48 96)
* Get alpha map from background captured image based on watermark size
* @param {number} size - Watermark size (48 or 96)
* @returns {Promise<Float32Array>} Alpha map
*/
async getAlphaMap(size) {
// 如果已缓存,直接返回
// If cached, return directly
if (this.alphaMaps[size]) {
return this.alphaMaps[size];
}
// 选择对应尺寸的背景捕获
// Select corresponding background capture based on watermark size
const bgImage = size === 48 ? this.bgCaptures.bg48 : this.bgCaptures.bg96;
// 创建临时 canvas 来提取 ImageData
// Create temporary canvas to extract ImageData
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
@ -103,54 +104,54 @@ export class WatermarkEngine {
const imageData = ctx.getImageData(0, 0, size, size);
// 计算 alpha map
// Calculate alpha map
const alphaMap = calculateAlphaMap(imageData);
// 缓存结果
// Cache result
this.alphaMaps[size] = alphaMap;
return alphaMap;
}
/**
* 移除图像上的水印
* @param {HTMLImageElement|HTMLCanvasElement} image - 输入图像
* @returns {Promise<HTMLCanvasElement>} 处理后的 canvas
* Remove watermark from image based on watermark size
* @param {HTMLImageElement|HTMLCanvasElement} image - Input image
* @returns {Promise<HTMLCanvasElement>} Processed canvas
*/
async removeWatermarkFromImage(image) {
// 创建 canvas
// Create canvas to process image
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
// 绘制原图
// Draw original image onto canvas
ctx.drawImage(image, 0, 0);
// 获取图像数据
// Get image data
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 检测水印配置
// Detect watermark configuration
const config = detectWatermarkConfig(canvas.width, canvas.height);
const position = calculateWatermarkPosition(canvas.width, canvas.height, config);
// 获取对应尺寸的 alpha map
// Get alpha map for watermark size
const alphaMap = await this.getAlphaMap(config.logoSize);
// 移除水印
// Remove watermark from image data
removeWatermark(imageData, alphaMap, position);
// 将处理后的数据写回 canvas
// Write processed image data back to canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
/**
* 获取水印信息用于显示
* @param {number} imageWidth - 图像宽度
* @param {number} imageHeight - 图像高度
* @returns {Object} 水印信息
* Get watermark information (for display)
* @param {number} imageWidth - Image width
* @param {number} imageHeight - Image height
* @returns {Object} Watermark information {size, position, config}
*/
getWatermarkInfo(imageWidth, imageHeight) {
const config = detectWatermarkConfig(imageWidth, imageHeight);