From fe814b17833c18486d66d7c7dabf9bb9a83ac31f Mon Sep 17 00:00:00 2001 From: Jad Date: Sat, 20 Dec 2025 14:11:23 +0800 Subject: [PATCH] docs: update docs --- README.md | 53 +++++++++++++++++++++------ README_zh.md | 50 ++++++++++++++++++++----- build.js | 8 ++-- src/core/alphaMap.js | 18 ++++----- src/core/blendModes.js | 48 ++++++++++++------------ src/core/watermarkEngine.js | 73 +++++++++++++++++++------------------ 6 files changed, 156 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 6edc369..0a6eb0e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/README_zh.md b/README_zh.md index b967198..187fb96 100644 --- a/README_zh.md +++ b/README_zh.md @@ -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 ``` ## 核心模块 diff --git a/build.js b/build.js index 3eea2df..46cdbbc 100644 --- a/build.js +++ b/build.js @@ -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'); diff --git a/src/core/alphaMap.js b/src/core/alphaMap.js index bc91557..ff955fa 100644 --- a/src/core/alphaMap.js +++ b/src/core/alphaMap.js @@ -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; } diff --git a/src/core/blendModes.js b/src/core/blendModes.js index 0489b56..6edfe51 100644 --- a/src/core/blendModes.js +++ b/src/core/blendModes.js @@ -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 } } } diff --git a/src/core/watermarkEngine.js b/src/core/watermarkEngine.js index 6e46b72..a87d01b 100644 --- a/src/core/watermarkEngine.js +++ b/src/core/watermarkEngine.js @@ -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} 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} 处理后的 canvas + * Remove watermark from image based on watermark size + * @param {HTMLImageElement|HTMLCanvasElement} image - Input image + * @returns {Promise} 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);