refactor: using esbuild
parent
4c905dcb10
commit
86a7033f3c
|
|
@ -5,6 +5,9 @@ node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.history
|
.history
|
||||||
|
|
||||||
|
# build
|
||||||
|
dist/
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -82,6 +82,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav class="flex gap-2 md:gap-6 text-sm font-medium text-gray-600 items-center">
|
<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://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>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="js/i18n.js"></script>
|
<script src="/i18n.js"></script>
|
||||||
<script type="module" src="js/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
|
@ -5,9 +5,8 @@
|
||||||
|
|
||||||
import { calculateAlphaMap } from './alphaMap.js';
|
import { calculateAlphaMap } from './alphaMap.js';
|
||||||
import { removeWatermark } from './blendModes.js';
|
import { removeWatermark } from './blendModes.js';
|
||||||
|
import BG_48_PATH from '../assets/bg_48.png';
|
||||||
const BG_48_PATH = '../assets/bg_48.png';
|
import BG_96_PATH from '../assets/bg_96.png';
|
||||||
const BG_96_PATH = '../assets/bg_96.png';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据图像尺寸检测水印配置
|
* 根据图像尺寸检测水印配置
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"title": "Gemini Watermark Remover - Lossless Watermark Removal Tool",
|
"title": "Gemini Watermark Remover - Lossless Watermark Removal Tool",
|
||||||
"header.title": "Gemini Watermark Remover",
|
"header.title": "Gemini Watermark Remover",
|
||||||
|
"nav.userscript": "Userscript for Gemini",
|
||||||
"nav.principle": "How It Works?",
|
"nav.principle": "How It Works?",
|
||||||
"main.title": "Gemini AI Watermark Removal",
|
"main.title": "Gemini AI Watermark Removal",
|
||||||
"main.subtitle": "Based on reverse alpha blending algorithm, pure browser-side processing, Free, Fast, and Lossless",
|
"main.subtitle": "Based on reverse alpha blending algorithm, pure browser-side processing, Free, Fast, and Lossless",
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"title": "Gemini Watermark Remover - Gemini 无损去水印工具",
|
"title": "Gemini Watermark Remover - Gemini 无损去水印工具",
|
||||||
"header.title": "Gemini Watermark Remover",
|
"header.title": "Gemini Watermark Remover",
|
||||||
|
"nav.userscript": "油猴脚本",
|
||||||
"nav.principle": "去水印原理",
|
"nav.principle": "去水印原理",
|
||||||
"main.title": "Gemini AI 图像去水印",
|
"main.title": "Gemini AI 图像去水印",
|
||||||
"main.subtitle": "基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损",
|
"main.subtitle": "基于反向 Alpha 混合算法,纯浏览器本地处理,免费、极速、无损",
|
||||||
|
|
@ -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
196
test.html
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
@ -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();
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "node build.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"esbuild": "^0.24.0"
|
|
||||||
},
|
|
||||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977"
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue