Created
February 17, 2026 18:55
-
-
Save minanagehsalalma/abd5c10d341d3a16c8a47f1a51268b7f to your computer and use it in GitHub Desktop.
Gemini Stupid watermark remover fixed by Claude.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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