docs: update docs
parent
23837b25ac
commit
fe814b1783
53
README.md
53
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
|
||||
|
|
|
|||
50
README_zh.md
50
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
|
||||
```
|
||||
|
||||
## 核心模块
|
||||
|
|
|
|||
8
build.js
8
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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue