Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active April 13, 2026 13:45
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.

Revisions

  1. minanagehsalalma revised this gist Apr 13, 2026. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions Gemini pure patchmatch.js
    Original file line number Diff line number Diff line change
    @@ -3,9 +3,9 @@
    // @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
    // @namespace https://github.com/minanagehsalalma
    // @version 3.0
    // @author Claude
    // @author Mina Nageh Salama
    // @match https://gemini.google.com/*
    // @grant none
    // @run-at document-end
  2. minanagehsalalma created this gist Feb 17, 2026.
    300 changes: 300 additions & 0 deletions Gemini pure patchmatch.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,300 @@
    // ==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();
    }

    })();