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 ## 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. 2. Drag and drop or click to select your Gemini-generated image.
3. The engine will automatically process and remove the watermark. 3. The engine will automatically process and remove the watermark.
4. Download the cleaned image. 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 ## How it Works
### The Gemini Watermarking Process ### The Gemini Watermarking Process
@ -83,20 +109,25 @@ By capturing the watermark on a known solid background, we reconstruct the exact
## Project Structure ## Project Structure
```text ```text
gemini-watermark-web/ gemini-watermark-remover/
├── index.html # Main entry point ├── public/
├── css/ │ ├── index.html # Main page
│ └── style.css # UI Styling │ └── terms.html # Terms of Service page
├── js/ ├── src/
│ ├── core/ │ ├── core/
│ │ ├── alphaMap.js # Alpha map calculation logic │ │ ├── alphaMap.js # Alpha map calculation logic
│ │ ├── blendModes.js # Implementation of Reverse Alpha Blending │ │ ├── blendModes.js # Implementation of Reverse Alpha Blending
│ │ └── watermarkEngine.js # Main engine coordinator │ │ └── watermarkEngine.js # Main engine coordinator
│ ├── assets/ │ ├── assets/
│ │ ├── bg-capture-48.png # Pre-captured 48×48 watermark map │ │ ├── bg_48.png # Pre-captured 48×48 watermark map
│ │ └── bg-capture-96.png # Pre-captured 96×96 watermark map │ │ └── bg_96.png # Pre-captured 96×96 watermark map
│ └── app.js # UI Interaction & Event handling │ ├── i18n/ # Internationalization language files
└── README.md │ ├── 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 ## Core Modules

View File

@ -46,11 +46,36 @@
## 使用方法 ## 使用方法
1. 在浏览器中打开 `index.html` ### 在线使用
1. 浏览器打开 [banana.ovo.re](https://banana.ovo.re)
2. 拖拽或点击选择带水印的 Gemini 图片 2. 拖拽或点击选择带水印的 Gemini 图片
3. 图片会自动开始处理,移除水印 3. 图片会自动开始处理,移除水印
4. 下载处理后的图片 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 添加水印的方式 ### Gemini 添加水印的方式
@ -83,20 +108,25 @@ $$original = \frac{watermarked - \alpha \cdot logo}{1 - \alpha}$$
## 项目结构 ## 项目结构
``` ```
gemini-watermark-web/ gemini-watermark-remover/
├── index.html # 主页面 ├── public/
├── css/ │ ├── index.html # 主页面
│ └── style.css # 样式文件 │ └── terms.html # 使用条款页面
├── js/ ├── src/
│ ├── core/ │ ├── core/
│ │ ├── alphaMap.js # Alpha map 计算 │ │ ├── alphaMap.js # Alpha map 计算
│ │ ├── blendModes.js # 反向 alpha 混合算法 │ │ ├── blendModes.js # 反向 alpha 混合算法
│ │ └── watermarkEngine.js # 主引擎 │ │ └── watermarkEngine.js # 主引擎
│ ├── assets/ │ ├── assets/
│ │ ├── bg-capture-48.png # 48×48 水印背景 │ │ ├── bg_48.png # 48×48 水印背景
│ │ └── bg-capture-96.png # 96×96 水印背景 │ │ └── bg_96.png # 96×96 水印背景
│ └── app.js # UI 交互逻辑 │ ├── i18n/ # 国际化语言文件
└── README.md │ ├── userscript/ # 用户脚本
│ ├── app.js # 网站应用入口
│ └── i18n.js # 国际化工具
├── dist/ # 构建输出目录
├── build.js # 构建脚本
└── package.json
``` ```
## 核心模块 ## 核心模块

View File

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

View File

@ -1,28 +1,28 @@
/** /**
* Alpha Map 计算模块 * Alpha Map calculator
* 从背景捕获图像计算 alpha 通道 * calculate alpha map from capture background image
*/ */
/** /**
* 从背景捕获图像计算 alpha map * Calculate alpha map from background captured image
* @param {ImageData} bgCaptureImageData - 背景捕获的 ImageData 对象 * @param {ImageData} bgCaptureImageData -ImageData object for background capture
* @returns {Float32Array} Alpha map (值范围 0.0-1.0) * @returns {Float32Array} Alpha map (value range 0.0-1.0)
*/ */
export function calculateAlphaMap(bgCaptureImageData) { export function calculateAlphaMap(bgCaptureImageData) {
const { width, height, data } = bgCaptureImageData; const { width, height, data } = bgCaptureImageData;
const alphaMap = new Float32Array(width * height); 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++) { 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 r = data[idx];
const g = data[idx + 1]; const g = data[idx + 1];
const b = data[idx + 2]; 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); const maxChannel = Math.max(r, g, b);
// 归一化到 [0, 1] 范围 // Normalize to [0, 1] range
alphaMap[i] = maxChannel / 255.0; alphaMap[i] = maxChannel / 255.0;
} }

View File

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

View File

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