Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Created February 17, 2026 18:55
Show Gist options
  • Select an option

  • Save minanagehsalalma/abd5c10d341d3a16c8a47f1a51268b7f to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/abd5c10d341d3a16c8a47f1a51268b7f to your computer and use it in GitHub Desktop.
Gemini Stupid watermark remover fixed by Claude.
// ==UserScript==
// @name 🍌 Gemini NanoBanana watermark replacer
// @description Removes Gemini watermark using patch-based texture synthesis.
// Finds the best matching texture patch nearby and blends it in.
// No data leaves your device.
// @namespace Claude
// @version 3.0
// @author Claude
// @match https://gemini.google.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const CONSTANTS = {
URL_PATTERN: /^https:\/\/lh3\.googleusercontent\.com\/rd-gg(?:-dl)?\/.+=s(?!0-d\?).*/
};
// ---------------------------------------------------------------------------
// Patch-Based Texture Inpainting
//
// Strategy:
// 1. Read a border ring (BORDER_W px wide) around the masked area.
// 2. Scan a search window in the surrounding image for the best-matching
// patch (lowest SSD against the known border pixels).
// 3. Copy that patch into the masked area.
// 4. Feather-blend the edges so there's no seam.
//
// Works great on repeating/near-uniform textures (cobblestone, sky, grass)
// which is exactly where Gemini places its watermark.
// ---------------------------------------------------------------------------
const BORDER_W = 14; // px of border context used for patch matching
const SEARCH_PAD = 180; // how far from the mask to search for patches (px)
const FEATHER = 12; // px of edge feathering for seamless blend
/**
* Get RGBA at (x, y), clamped to image bounds.
*/
function getPixel(data, W, H, x, y) {
x = Math.max(0, Math.min(W - 1, x));
y = Math.max(0, Math.min(H - 1, y));
const i = (y * W + x) * 4;
return [data[i], data[i+1], data[i+2]];
}
function setPixel(data, W, x, y, r, g, b) {
const i = (y * W + x) * 4;
data[i] = r;
data[i+1] = g;
data[i+2] = b;
data[i+3] = 255;
}
/**
* Sum of Squared Differences between two patches, only sampling
* the border ring (known pixels) for the comparison.
*
* Patch A: centred on (ax, ay) — from the masked region (border only).
* Patch B: centred on (bx, by) — candidate from search window.
*
* halfW / halfH : half-extents of the full patch
*/
function patchSSD(data, W, H, ax, ay, bx, by, halfW, halfH) {
let ssd = 0, count = 0;
for (let dy = -halfH; dy <= halfH; dy++) {
for (let dx = -halfW; dx <= halfW; dx++) {
// Only compare border ring pixels (skip the interior mask)
const insideX = Math.abs(dx) < halfW - BORDER_W;
const insideY = Math.abs(dy) < halfH - BORDER_W;
if (insideX && insideY) continue;
const [rA, gA, bA] = getPixel(data, W, H, ax + dx, ay + dy);
const [rB, gB, bB] = getPixel(data, W, H, bx + dx, by + dy);
ssd += (rA-rB)**2 + (gA-gB)**2 + (bA-bB)**2;
count++;
}
}
return count > 0 ? ssd / count : INF;
}
const INF = 1e12;
/**
* Main patch inpaint function.
* @param {Uint8ClampedArray} data - RGBA flat array (modified in-place)
* @param {number} W
* @param {number} H
* @param {number} mx - mask top-left x
* @param {number} my - mask top-left y
* @param {number} mw - mask width
* @param {number} mh - mask height
*/
function patchInpaint(data, W, H, mx, my, mw, mh) {
const cx = Math.round(mx + mw / 2); // mask centre
const cy = Math.round(my + mh / 2);
const halfW = Math.round(mw / 2) + BORDER_W;
const halfH = Math.round(mh / 2) + BORDER_W;
// --- Find best matching patch in surrounding search window ---
let bestX = -1, bestY = -1, bestSSD = INF;
// Search window bounds — avoid the mask itself and image edges
const sx0 = Math.max(halfW, cx - SEARCH_PAD);
const sx1 = Math.min(W - halfW, cx + SEARCH_PAD);
const sy0 = Math.max(halfH, cy - SEARCH_PAD);
const sy1 = Math.min(H - halfH, cy + SEARCH_PAD);
const STEP = Math.max(1, Math.round(Math.min(mw, mh) / 8)); // skip pixels for speed
for (let y = sy0; y <= sy1; y += STEP) {
for (let x = sx0; x <= sx1; x += STEP) {
// Skip candidates that overlap the mask
if (Math.abs(x - cx) < halfW && Math.abs(y - cy) < halfH) continue;
const ssd = patchSSD(data, W, H, cx, cy, x, y, halfW, halfH);
if (ssd < bestSSD) { bestSSD = ssd; bestX = x; bestY = y; }
}
}
if (bestX < 0) {
// Fallback: bilinear edge blend (same as v1)
fallbackBlend(data, W, H, mx, my, mw, mh);
return;
}
// Refine: local dense search around the winner
for (let y = bestY - STEP; y <= bestY + STEP; y++) {
for (let x = bestX - STEP; x <= bestX + STEP; x++) {
if (x < halfW || x >= W - halfW || y < halfH || y >= H - halfH) continue;
if (Math.abs(x - cx) < halfW && Math.abs(y - cy) < halfH) continue;
const ssd = patchSSD(data, W, H, cx, cy, x, y, halfW, halfH);
if (ssd < bestSSD) { bestSSD = ssd; bestX = x; bestY = y; }
}
}
const offX = bestX - cx;
const offY = bestY - cy;
// --- Copy best patch into masked area, with feathering ---
for (let row = 0; row < mh; row++) {
for (let col = 0; col < mw; col++) {
const tx = mx + col;
const ty = my + row;
// Distance from each edge of the mask (for feathering weight)
const edgeDist = Math.min(col, row, mw - 1 - col, mh - 1 - row);
const alpha = Math.min(1, edgeDist / FEATHER); // 0 at edge, 1 in centre
const [rSrc, gSrc, bSrc] = getPixel(data, W, H, tx + offX, ty + offY);
if (alpha >= 1) {
// Pure patch copy in the interior
setPixel(data, W, tx, ty, rSrc, gSrc, bSrc);
} else {
// Blend with original border pixels at the seam
const [rOrig, gOrig, bOrig] = getPixel(data, W, H, tx, ty);
setPixel(data, W, tx, ty,
Math.round(rOrig * (1 - alpha) + rSrc * alpha),
Math.round(gOrig * (1 - alpha) + gSrc * alpha),
Math.round(bOrig * (1 - alpha) + bSrc * alpha)
);
}
}
}
}
/** Bilinear edge-blend fallback (v1 approach) */
function fallbackBlend(data, W, H, mx, my, mw, mh) {
for (let row = 0; row < mh; row++) {
for (let col = 0; col < mw; col++) {
const tx = (col / (mw - 1));
const ty = (row / (mh - 1));
const [rL, gL, bL] = getPixel(data, W, H, mx - 1, my + row);
const [rR, gR, bR] = getPixel(data, W, H, mx + mw, my + row);
const [rT, gT, bT] = getPixel(data, W, H, mx + col, my - 1);
const [rB, gB, bB] = getPixel(data, W, H, mx + col, my + mh);
const r = (rL*(1-tx) + rR*tx) * 0.5 + (rT*(1-ty) + rB*ty) * 0.5;
const g = (gL*(1-tx) + gR*tx) * 0.5 + (gT*(1-ty) + gB*ty) * 0.5;
const b = (bL*(1-tx) + bR*tx) * 0.5 + (bT*(1-ty) + bB*ty) * 0.5;
setPixel(data, W, mx + col, my + row, Math.round(r), Math.round(g), Math.round(b));
}
}
}
// ---------------------------------------------------------------------------
// Watermark Engine
// ---------------------------------------------------------------------------
class WatermarkEngine {
getWatermarkConfig(width, height) {
return (width > 1024 && height > 1024)
? { logoSize: 96, margin: 64 }
: { logoSize: 48, margin: 32 };
}
async processImage(imgSource) {
const canvas = document.createElement('canvas');
canvas.width = imgSource.width;
canvas.height = imgSource.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgSource, 0, 0);
const W = canvas.width, H = canvas.height;
const imageData = ctx.getImageData(0, 0, W, H);
const { logoSize, margin } = this.getWatermarkConfig(W, H);
const mx = W - margin - logoSize;
const my = H - margin - logoSize;
patchInpaint(imageData.data, W, H, mx, my, logoSize, logoSize);
ctx.putImageData(imageData, 0, 0);
return canvas;
}
}
// ---------------------------------------------------------------------------
// Main Controller
// ---------------------------------------------------------------------------
class GeminiPure {
constructor() {
this.engine = new WatermarkEngine();
this.setupNetworkInterceptor();
this.setupDOMObserver();
this.log('Active (patch-match v3). Waiting for images...');
}
log(msg, ...args) {
console.log(
`%c Gemini Pure %c ${msg}`,
'background:#8b5cf6; color:white; padding:2px 6px; border-radius:4px;',
'color:#a78bfa;',
...args
);
}
setupNetworkInterceptor() {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (CONSTANTS.URL_PATTERN.test(url)) {
this.log('Intercepting + patch-inpainting:', url);
const cleanUrl = url.replace(/=s\d+(?=[-?#]|$)/, '=s0');
if (typeof args[0] === 'string') args[0] = cleanUrl;
else if (args[0]?.url) args[0].url = cleanUrl;
const response = await originalFetch(...args);
if (!response.ok) return response;
try {
const blob = await response.blob();
const processedBlob = await this.cleanImageBlob(blob);
return new Response(processedBlob, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
} catch (err) {
console.warn('[Gemini Pure] Processing failed, returning original:', err);
return response;
}
}
return originalFetch(...args);
};
}
async cleanImageBlob(blob) {
const bitmap = await createImageBitmap(blob);
const cleanCanvas = await this.engine.processImage(bitmap);
return new Promise(resolve => cleanCanvas.toBlob(resolve, 'image/png'));
}
setupDOMObserver() {
new MutationObserver((mutations) => {
if (mutations.some(m => m.addedNodes.length)) this.processExistingImages();
}).observe(document.body, { childList: true, subtree: true });
}
processExistingImages() {
document.querySelectorAll(
'img[src*="googleusercontent.com"]:not([data-gp-processed])'
).forEach(img => {
if (!img.closest('generated-image, .generated-image-container')) return;
img.dataset.gpProcessed = 'true';
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => new GeminiPure());
} else {
new GeminiPure();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment