refactor: using esbuild
parent
4c905dcb10
commit
86a7033f3c
|
|
@ -5,6 +5,9 @@ node_modules
|
|||
.DS_Store
|
||||
.history
|
||||
|
||||
# build
|
||||
dist/
|
||||
|
||||
# debug
|
||||
npm-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>
|
||||
</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>
|
||||
|
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 { 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';
|
||||
|
||||
/**
|
||||
* 根据图像尺寸检测水印配置
|
||||
|
|
@ -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",
|
||||
|
|
@ -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 混合算法,纯浏览器本地处理,免费、极速、无损",
|
||||
|
|
@ -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