feat: add userscript

main
Jad 2025-12-20 09:29:00 +08:00
parent a87d111e1b
commit 4c905dcb10
6 changed files with 823 additions and 0 deletions

15
.gitignore vendored 100644
View File

@ -0,0 +1,15 @@
# dependencies
node_modules
# misc
.DS_Store
.history
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
.env

View File

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

188
userscript/index.js 100644
View File

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

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

View File

@ -0,0 +1,275 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
esbuild:
specifier: ^0.24.0
version: 0.24.2
packages:
'@esbuild/aix-ppc64@0.24.2':
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.24.2':
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.24.2':
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.24.2':
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.24.2':
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.24.2':
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.24.2':
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.24.2':
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.24.2':
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.24.2':
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.24.2':
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.24.2':
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.24.2':
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.24.2':
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.24.2':
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.24.2':
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.24.2':
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.24.2':
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.24.2':
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.24.2':
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.24.2':
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.24.2':
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.24.2':
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.24.2':
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.24.2':
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
esbuild@0.24.2:
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
engines: {node: '>=18'}
hasBin: true
snapshots:
'@esbuild/aix-ppc64@0.24.2':
optional: true
'@esbuild/android-arm64@0.24.2':
optional: true
'@esbuild/android-arm@0.24.2':
optional: true
'@esbuild/android-x64@0.24.2':
optional: true
'@esbuild/darwin-arm64@0.24.2':
optional: true
'@esbuild/darwin-x64@0.24.2':
optional: true
'@esbuild/freebsd-arm64@0.24.2':
optional: true
'@esbuild/freebsd-x64@0.24.2':
optional: true
'@esbuild/linux-arm64@0.24.2':
optional: true
'@esbuild/linux-arm@0.24.2':
optional: true
'@esbuild/linux-ia32@0.24.2':
optional: true
'@esbuild/linux-loong64@0.24.2':
optional: true
'@esbuild/linux-mips64el@0.24.2':
optional: true
'@esbuild/linux-ppc64@0.24.2':
optional: true
'@esbuild/linux-riscv64@0.24.2':
optional: true
'@esbuild/linux-s390x@0.24.2':
optional: true
'@esbuild/linux-x64@0.24.2':
optional: true
'@esbuild/netbsd-arm64@0.24.2':
optional: true
'@esbuild/netbsd-x64@0.24.2':
optional: true
'@esbuild/openbsd-arm64@0.24.2':
optional: true
'@esbuild/openbsd-x64@0.24.2':
optional: true
'@esbuild/sunos-x64@0.24.2':
optional: true
'@esbuild/win32-arm64@0.24.2':
optional: true
'@esbuild/win32-ia32@0.24.2':
optional: true
'@esbuild/win32-x64@0.24.2':
optional: true
esbuild@0.24.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.24.2
'@esbuild/android-arm': 0.24.2
'@esbuild/android-arm64': 0.24.2
'@esbuild/android-x64': 0.24.2
'@esbuild/darwin-arm64': 0.24.2
'@esbuild/darwin-x64': 0.24.2
'@esbuild/freebsd-arm64': 0.24.2
'@esbuild/freebsd-x64': 0.24.2
'@esbuild/linux-arm': 0.24.2
'@esbuild/linux-arm64': 0.24.2
'@esbuild/linux-ia32': 0.24.2
'@esbuild/linux-loong64': 0.24.2
'@esbuild/linux-mips64el': 0.24.2
'@esbuild/linux-ppc64': 0.24.2
'@esbuild/linux-riscv64': 0.24.2
'@esbuild/linux-s390x': 0.24.2
'@esbuild/linux-x64': 0.24.2
'@esbuild/netbsd-arm64': 0.24.2
'@esbuild/netbsd-x64': 0.24.2
'@esbuild/openbsd-arm64': 0.24.2
'@esbuild/openbsd-x64': 0.24.2
'@esbuild/sunos-x64': 0.24.2
'@esbuild/win32-arm64': 0.24.2
'@esbuild/win32-ia32': 0.24.2
'@esbuild/win32-x64': 0.24.2