refactor: using esbuild

main
Jad 2025-12-20 12:31:57 +08:00
parent 4c905dcb10
commit 86a7033f3c
21 changed files with 237 additions and 734 deletions

3
.gitignore vendored
View File

@ -5,6 +5,9 @@ node_modules
.DS_Store
.history
# build
dist/
# debug
npm-debug.log*
yarn-debug.log*

68
build.js 100644
View File

@ -0,0 +1,68 @@
import * as esbuild from 'esbuild';
import { cp, mkdir } from 'fs/promises';
const userscriptBanner = `// ==UserScript==
// @name Gemini NanoBanana Watermark Remover
// @name:zh-CN Gemini NanoBanana 图片水印移除
// @namespace https://github.com/journey-ad
// @version 0.1.0
// @description Automatically removes watermarks from Gemini AI generated images
// @description:zh-CN 自动移除 Gemini AI 生成图像中的水印
// @icon https://www.google.com/s2/favicons?domain=gemini.google.com
// @author journey-ad
// @license MIT
// @match https://gemini.google.com/app/*
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
`;
async function build() {
console.log(`Start Build... ${process.env.NODE_ENV === 'production' ? 'production' : 'development'}\r\n`);
// 构建网站 - app.js
console.log('Building: dist/app.js');
await esbuild.build({
entryPoints: ['src/app.js'],
bundle: true,
format: 'esm',
outfile: 'dist/app.js',
loader: { '.png': 'file' },
assetNames: 'assets/[name]',
publicPath: '/',
minify: process.env.NODE_ENV === 'production'
});
// 构建网站 - i18n.js
console.log('Building: dist/i18n.js');
await esbuild.build({
entryPoints: ['src/i18n.js'],
bundle: true,
format: 'esm',
outfile: 'dist/i18n.js',
minify: process.env.NODE_ENV === 'production'
});
// 构建油猴脚本
console.log('Building: dist/userscript/gemini-watermark-remover.user.js');
await mkdir('dist/userscript', { recursive: true });
await esbuild.build({
entryPoints: ['src/userscript/index.js'],
bundle: true,
format: 'iife',
outfile: 'dist/userscript/gemini-watermark-remover.user.js',
banner: { js: userscriptBanner },
loader: { '.png': 'dataurl' },
minify: false
});
// 复制静态文件
console.log('Copying: src/i18n -> dist/i18n');
await cp('src/i18n', 'dist/i18n', { recursive: true });
console.log('Copying: public -> dist');
await cp('public', 'dist', { recursive: true });
console.log('\r\n✓ Build complete');
}
build().catch(console.error);

14
package.json 100644
View File

@ -0,0 +1,14 @@
{
"name": "gemini-watermark-remover",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "node build.js",
"build": "NODE_ENV=production node build.js",
"serve": "npx serve dist"
},
"devDependencies": {
"esbuild": "^0.24.0"
},
"packageManager": "pnpm@10.11.0"
}

View File

@ -82,6 +82,7 @@
</h1>
</div>
<nav class="flex gap-2 md:gap-6 text-sm font-medium text-gray-600 items-center">
<a href="/userscript/gemini-watermark-remover.user.js" target="_blank" class="hidden md:inline-block hover:text-primary transition-colors" data-i18n="nav.userscript">油猴脚本</a>
<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>
@ -264,7 +265,7 @@
</div>
</div>
<script type="module" src="js/i18n.js"></script>
<script type="module" src="js/app.js"></script>
<script src="/i18n.js"></script>
<script src="/app.js"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -5,9 +5,8 @@
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';
import BG_48_PATH from '../assets/bg_48.png';
import BG_96_PATH from '../assets/bg_96.png';
/**
* 根据图像尺寸检测水印配置

View File

@ -1,6 +1,7 @@
{
"title": "Gemini Watermark Remover - Lossless Watermark Removal Tool",
"header.title": "Gemini Watermark Remover",
"nav.userscript": "Userscript for Gemini",
"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",

View File

@ -1,6 +1,7 @@
{
"title": "Gemini Watermark Remover - Gemini 无损去水印工具",
"header.title": "Gemini Watermark Remover",
"nav.userscript": "油猴脚本",
"nav.principle": "去水印原理",
"main.title": "Gemini AI 图像去水印",
"main.subtitle": "基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损",

View File

@ -0,0 +1,145 @@
import { WatermarkEngine } from '../core/watermarkEngine.js';
let engine = null;
const processingQueue = new Set();
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
const loadImage = (src) => new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
const canvasToBlob = (canvas, type = 'image/png') =>
new Promise(resolve => canvas.toBlob(resolve, type));
const isValidGeminiImage = (img) =>
/=s\d+\-rj/.test(img.src) || (img.naturalWidth >= 256 && img.naturalHeight >= 256);
const findGeminiImages = () =>
[...document.querySelectorAll('img[src*="googleusercontent.com"]')].filter(isValidGeminiImage);
const fetchBlob = (url) => new Promise((resolve, reject) => {
// use GM_xmlhttpRequest to fetch image blob to avoid cross-origin issue
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'blob',
onload: (response) => resolve(response.response),
onerror: reject
});
});
const replaceWithNormalSize = (src) => {
// use normal size image to fit watermark
return src.replace(/=s\d+.+$/, '=s0');
}
async function processImage(imgElement) {
if (!engine || processingQueue.has(imgElement)) return;
processingQueue.add(imgElement);
imgElement.dataset.watermarkProcessed = 'processing';
try {
if (!isValidGeminiImage(imgElement)) {
imgElement.dataset.watermarkProcessed = 'skipped';
return;
}
const originalSrc = imgElement.src;
imgElement.src = '';
imgElement.dataset.src = originalSrc;
const blob = await fetchBlob(replaceWithNormalSize(originalSrc));
const blobUrl = URL.createObjectURL(blob);
const img = await loadImage(blobUrl);
const canvas = await engine.removeWatermarkFromImage(img);
const processedBlob = await canvasToBlob(canvas);
URL.revokeObjectURL(blobUrl);
imgElement.src = URL.createObjectURL(processedBlob);
imgElement.dataset.watermarkProcessed = 'true';
console.log('[Gemini Watermark Remover] Processed image');
} catch (error) {
console.warn('[Gemini Watermark Remover] Failed to process image:', error);
imgElement.dataset.watermarkProcessed = 'failed';
} finally {
processingQueue.delete(imgElement);
}
}
const processAllImages = () => {
const images = findGeminiImages();
console.log(`[Gemini Watermark Remover] Found ${images.length} images to process`);
images.forEach(processImage);
};
const setupMutationObserver = () => {
new MutationObserver(debounce(processAllImages, 100))
.observe(document.body, { childList: true, subtree: true });
console.log('[Gemini Watermark Remover] MutationObserver active');
};
async function processImageBlob(blob) {
const blobUrl = URL.createObjectURL(blob);
const img = await loadImage(blobUrl);
const canvas = await engine.removeWatermarkFromImage(img);
URL.revokeObjectURL(blobUrl);
return canvasToBlob(canvas);
}
const GEMINI_URL_PATTERN = /^https:\/\/lh3\.googleusercontent\.com\/rd-gg(-dl)?\//; // downloadable image url pattern
// Intercept fetch requests to replace downloadable image with the watermark removed image
const { fetch: origFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (GEMINI_URL_PATTERN.test(url)) {
console.log('[Gemini Watermark Remover] Intercepting:', url);
const origUrl = replaceWithNormalSize(url);
if (typeof args[0] === 'string') args[0] = origUrl;
else if (args[0]?.url) args[0].url = origUrl;
const response = await origFetch(...args);
if (!engine || !response.ok) return response;
try {
const processedBlob = await processImageBlob(await response.blob());
return new Response(processedBlob, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
} catch (error) {
console.warn('[Gemini Watermark Remover] Processing failed:', error);
return response;
}
}
return origFetch(...args);
};
(async function init() {
try {
console.log('[Gemini Watermark Remover] Initializing...');
engine = await WatermarkEngine.create();
processAllImages();
setupMutationObserver();
console.log('[Gemini Watermark Remover] Ready');
} catch (error) {
console.error('[Gemini Watermark Remover] Initialization failed:', error);
}
})();

196
test.html
View File

@ -1,196 +0,0 @@
<!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>

View File

@ -1,26 +0,0 @@
import * as esbuild from 'esbuild';
const banner = `// ==UserScript==
// @name Gemini Watermark Remover
// @namespace https://github.com/journey-ad
// @version 1.0.0
// @description Automatically removes watermarks from Gemini AI generated images
// @author journey-ad
// @match https://gemini.google.com/app/*
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
`;
await esbuild.build({
entryPoints: ['index.js'],
bundle: true,
outfile: 'dist/gemini-watermark-remover.user.js',
format: 'iife',
banner: { js: banner },
loader: {
'.png': 'dataurl'
}
});
console.log('✓ Build complete: gemini-watermark-remover.user.js');

File diff suppressed because one or more lines are too long

View File

@ -1,188 +0,0 @@
import { WatermarkEngine } from '../js/core/watermarkEngine.js';
import BG_48_PATH from '../assets/bg_48.png';
import BG_96_PATH from '../assets/bg_96.png';
// ============ DOM UTILITIES ============
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function findGeminiImages() {
return [...document.querySelectorAll('img[src*="googleusercontent.com"]')].filter(isValidGeminiImage);
}
function isValidGeminiImage(img) {
if (/=s\d+\-rj/.test(img.src)) return true;
return img.naturalWidth >= 256 && img.naturalHeight >= 256;
}
// ============ IMAGE PROCESSING ============
let engine = null;
const processingQueue = new Set();
async function processImage(imgElement) {
if (!engine || processingQueue.has(imgElement)) return;
try {
imgElement.dataset.watermarkProcessed = 'processing';
processingQueue.add(imgElement);
if (!isValidGeminiImage(imgElement)) {
imgElement.dataset.watermarkProcessed = 'skipped';
return;
}
const src = imgElement.src;
imgElement.src = '';
imgElement.dataset.src = src;
const blob = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: src.replace(/=s\d+.+$/, '=s0'),
responseType: 'blob',
onload: (response) => resolve(response.response),
onerror: reject
});
});
const blobUrl = URL.createObjectURL(blob);
const tempImg = new Image();
await new Promise((resolve, reject) => {
tempImg.onload = resolve;
tempImg.onerror = reject;
tempImg.src = blobUrl;
});
const resultCanvas = await engine.removeWatermarkFromImage(tempImg);
const processedBlob = await new Promise((resolve) => {
resultCanvas.toBlob(resolve, 'image/png');
});
URL.revokeObjectURL(blobUrl);
imgElement.src = URL.createObjectURL(processedBlob);
imgElement.dataset.watermarkProcessed = 'true';
console.log('[Gemini Watermark Remover] Processed image');
} catch (error) {
console.warn('[Gemini Watermark Remover] Failed to process image:', error);
imgElement.dataset.watermarkProcessed = 'failed';
} finally {
processingQueue.delete(imgElement);
}
}
async function processAllImages() {
const images = findGeminiImages();
console.log(`[Gemini Watermark Remover] Found ${images.length} images to process`);
for (const img of images) {
processImage(img);
}
}
// ============ MUTATION OBSERVER ============
function setupMutationObserver() {
const observer = new MutationObserver(debounce(() => {
processAllImages();
}, 100));
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('[Gemini Watermark Remover] MutationObserver active');
}
// ============ FETCH INTERCEPTION ============
async function processImageBlob(blob) {
const blobUrl = URL.createObjectURL(blob);
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = blobUrl;
});
const resultCanvas = await engine.removeWatermarkFromImage(img);
URL.revokeObjectURL(blobUrl);
return new Promise((resolve) => {
resultCanvas.toBlob(resolve, 'image/png');
});
}
async function getImage(base64) {
const img = new Image();
img.src = base64;
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
});
return img;
}
const { fetch: origFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (/^https:\/\/lh3\.googleusercontent\.com\/rd-gg(-dl)?\//.test(url)) {
console.log('[Gemini Watermark Remover] Intercepting:', url);
const origUrl = url.replace(/=s\d+.+$/, '=s0');
if (typeof args[0] === 'string') {
args[0] = origUrl;
} else if (args[0]?.url) {
args[0].url = origUrl;
}
const response = await origFetch(...args);
if (!engine || !response.ok) {
return response;
}
try {
const blob = await response.blob();
const processedBlob = await processImageBlob(blob);
return new Response(processedBlob, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
} catch (error) {
console.warn('[Gemini Watermark Remover] Processing failed:', error);
return response;
}
}
return origFetch(...args);
};
// ============ INITIALIZATION ============
async function init() {
try {
console.log('[Gemini Watermark Remover] Initializing...');
engine = new WatermarkEngine({
bg48: await getImage(BG_48_PATH),
bg96: await getImage(BG_96_PATH)
});
await processAllImages();
setupMutationObserver();
console.log('[Gemini Watermark Remover] Ready');
} catch (error) {
console.error('[Gemini Watermark Remover] Initialization failed:', error);
}
}
init();

View File

@ -1,10 +0,0 @@
{
"type": "module",
"scripts": {
"build": "node build.js"
},
"devDependencies": {
"esbuild": "^0.24.0"
},
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
}