GitHub Avatar to Identicon Switcher

Replace GitHub avatars with identicons - toggle in Tampermonkey menu

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         GitHub Avatar to Identicon Switcher
// @namespace    http://tampermonkey.net/
// @version      2025-11-09
// @description  Replace GitHub avatars with identicons - toggle in Tampermonkey menu
// @author       TheMX/TheXM
// @license      MIT
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAaQAAAGkAQMAAABEgsN2AAAABlBMVEXw8PCgjuCi1ezxAAAAeElEQVR4Ae3LoRWAMBBEwaAog1JDqSkDRTAJ7hy8nJgv9+0USZL0e7VHNYqiKIqiKIqiKIpKpSRJkiRJSt7RR+cYtjlc4YWiKIqiKIqiKIqiVqi32qNaiaMoiqIoiqIoiqKodeqew05RFEVRFEVRFEVROZUkSfqmBwlwQYdil+rrAAAAAElFTkSuQmCC
// @match        https://github.com/*
// @match        https://*.github.com/*
// @match        https://avatars.githubusercontent.com/*
// @match        https://*.githubusercontent.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @antifeature  none
// ==/UserScript==

// Credits to Joseph K. Myers for https://www.myersdaily.org/joseph/javascript/md5-text.html
function md5cycle(x, k) {
  var a = x[0],
    b = x[1],
    c = x[2],
    d = x[3];

  a = ff(a, b, c, d, k[0], 7, -680876936);
  d = ff(d, a, b, c, k[1], 12, -389564586);
  c = ff(c, d, a, b, k[2], 17, 606105819);
  b = ff(b, c, d, a, k[3], 22, -1044525330);
  a = ff(a, b, c, d, k[4], 7, -176418897);
  d = ff(d, a, b, c, k[5], 12, 1200080426);
  c = ff(c, d, a, b, k[6], 17, -1473231341);
  b = ff(b, c, d, a, k[7], 22, -45705983);
  a = ff(a, b, c, d, k[8], 7, 1770035416);
  d = ff(d, a, b, c, k[9], 12, -1958414417);
  c = ff(c, d, a, b, k[10], 17, -42063);
  b = ff(b, c, d, a, k[11], 22, -1990404162);
  a = ff(a, b, c, d, k[12], 7, 1804603682);
  d = ff(d, a, b, c, k[13], 12, -40341101);
  c = ff(c, d, a, b, k[14], 17, -1502002290);
  b = ff(b, c, d, a, k[15], 22, 1236535329);

  a = gg(a, b, c, d, k[1], 5, -165796510);
  d = gg(d, a, b, c, k[6], 9, -1069501632);
  c = gg(c, d, a, b, k[11], 14, 643717713);
  b = gg(b, c, d, a, k[0], 20, -373897302);
  a = gg(a, b, c, d, k[5], 5, -701558691);
  d = gg(d, a, b, c, k[10], 9, 38016083);
  c = gg(c, d, a, b, k[15], 14, -660478335);
  b = gg(b, c, d, a, k[4], 20, -405537848);
  a = gg(a, b, c, d, k[9], 5, 568446438);
  d = gg(d, a, b, c, k[14], 9, -1019803690);
  c = gg(c, d, a, b, k[3], 14, -187363961);
  b = gg(b, c, d, a, k[8], 20, 1163531501);
  a = gg(a, b, c, d, k[13], 5, -1444681467);
  d = gg(d, a, b, c, k[2], 9, -51403784);
  c = gg(c, d, a, b, k[7], 14, 1735328473);
  b = gg(b, c, d, a, k[12], 20, -1926607734);

  a = hh(a, b, c, d, k[5], 4, -378558);
  d = hh(d, a, b, c, k[8], 11, -2022574463);
  c = hh(c, d, a, b, k[11], 16, 1839030562);
  b = hh(b, c, d, a, k[14], 23, -35309556);
  a = hh(a, b, c, d, k[1], 4, -1530992060);
  d = hh(d, a, b, c, k[4], 11, 1272893353);
  c = hh(c, d, a, b, k[7], 16, -155497632);
  b = hh(b, c, d, a, k[10], 23, -1094730640);
  a = hh(a, b, c, d, k[13], 4, 681279174);
  d = hh(d, a, b, c, k[0], 11, -358537222);
  c = hh(c, d, a, b, k[3], 16, -722521979);
  b = hh(b, c, d, a, k[6], 23, 76029189);
  a = hh(a, b, c, d, k[9], 4, -640364487);
  d = hh(d, a, b, c, k[12], 11, -421815835);
  c = hh(c, d, a, b, k[15], 16, 530742520);
  b = hh(b, c, d, a, k[2], 23, -995338651);

  a = ii(a, b, c, d, k[0], 6, -198630844);
  d = ii(d, a, b, c, k[7], 10, 1126891415);
  c = ii(c, d, a, b, k[14], 15, -1416354905);
  b = ii(b, c, d, a, k[5], 21, -57434055);
  a = ii(a, b, c, d, k[12], 6, 1700485571);
  d = ii(d, a, b, c, k[3], 10, -1894986606);
  c = ii(c, d, a, b, k[10], 15, -1051523);
  b = ii(b, c, d, a, k[1], 21, -2054922799);
  a = ii(a, b, c, d, k[8], 6, 1873313359);
  d = ii(d, a, b, c, k[15], 10, -30611744);
  c = ii(c, d, a, b, k[6], 15, -1560198380);
  b = ii(b, c, d, a, k[13], 21, 1309151649);
  a = ii(a, b, c, d, k[4], 6, -145523070);
  d = ii(d, a, b, c, k[11], 10, -1120210379);
  c = ii(c, d, a, b, k[2], 15, 718787259);
  b = ii(b, c, d, a, k[9], 21, -343485551);

  x[0] = add32(a, x[0]);
  x[1] = add32(b, x[1]);
  x[2] = add32(c, x[2]);
  x[3] = add32(d, x[3]);
}

function cmn(q, a, b, x, s, t) {
  a = add32(add32(a, q), add32(x, t));
  return add32((a << s) | (a >>> (32 - s)), b);
}

function ff(a, b, c, d, x, s, t) {
  return cmn((b & c) | (~b & d), a, b, x, s, t);
}

function gg(a, b, c, d, x, s, t) {
  return cmn((b & d) | (c & ~d), a, b, x, s, t);
}

function hh(a, b, c, d, x, s, t) {
  return cmn(b ^ c ^ d, a, b, x, s, t);
}

function ii(a, b, c, d, x, s, t) {
  return cmn(c ^ (b | ~d), a, b, x, s, t);
}

function md51(s) {
  txt = "";
  var n = s.length,
    state = [1732584193, -271733879, -1732584194, 271733878],
    i;
  for (i = 64; i <= s.length; i += 64) {
    md5cycle(state, md5blk(s.substring(i - 64, i)));
  }
  s = s.substring(i - 64);
  var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
  for (i = 0; i < s.length; i++) {
    tail[i >> 2] |= s.charCodeAt(i) << (i % 4 << 3);
  }
  tail[i >> 2] |= 0x80 << (i % 4 << 3);
  if (i > 55) {
    md5cycle(state, tail);
    for (i = 0; i < 16; i++) tail[i] = 0;
  }
  tail[14] = n * 8;
  md5cycle(state, tail);
  return state;
}

function md5blk(s) {
  var md5blks = [],
    i;
  for (i = 0; i < 64; i += 4) {
    md5blks[i >> 2] =
      s.charCodeAt(i) +
      (s.charCodeAt(i + 1) << 8) +
      (s.charCodeAt(i + 2) << 16) +
      (s.charCodeAt(i + 3) << 24);
  }
  return md5blks;
}

var hex_chr = "0123456789abcdef".split("");

function rhex(n) {
  var s = "",
    j = 0;
  for (; j < 4; j++) {
    s += hex_chr[(n >> (j * 8 + 4)) & 0x0f] + hex_chr[(n >> (j * 8)) & 0x0f];
  }
  return s;
}

function hex(x) {
  for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]);
  return x.join("");
}

function md5(s) {
  return hex(md51(s));
}

function add32(a, b) {
  return (a + b) & 0xffffffff;
}

if (md5("hello") != "5d41402abc4b2a76b9719d911017c592") {
  function add32(x, y) {
    var lsw = (x & 0xffff) + (y & 0xffff),
      msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xffff);
  }
}

// Credits to David Graham for https://github.com/dgraham/identicon that I ported to C++ and now to JavaScript
(function () {
  "use strict";

  const CONFIG = {
    enabled: GM_getValue("identiconEnabled", false),
    avatarDomains: ["avatars.githubusercontent.com"],
  };

  let menuCommandId;

  function registerMenuCommand() {
    if (menuCommandId) {
      GM_unregisterMenuCommand(menuCommandId);
    }

    menuCommandId = GM_registerMenuCommand(
      CONFIG.enabled ? "🔴 Disable Identicons" : "🟢 Enable Identicons",
      function () {
        CONFIG.enabled = !CONFIG.enabled;
        GM_setValue("identiconEnabled", CONFIG.enabled);
        registerMenuCommand();
        replaceAllAvatars();
      },
      CONFIG.enabled ? "d" : "e"
    );
  }

  function isAvatarUrl(url) {
    return CONFIG.avatarDomains.some((domain) => url.includes(domain));
  }

  function hexToBytes(hex) {
    const out = new Uint8Array(16);
    for (let i = 0; i < 16; ++i) {
      out[i] = parseInt(hex.substr(i * 2, 2), 16);
    }
    return out;
  }

  function generateIdenticon(identifier, size = 420) {
    if (identifier === "error") return null;

    const GRID_SIZE = 5;
    const SQUARE_SIZE = 70; // 420 / 6 = 70 (5 squares + margins)
    const MARGIN = SQUARE_SIZE / 2; // 35px margin

    const canvas = document.createElement("canvas");
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext("2d");

    const md5hex = md5(identifier);
    const hashBytes = hexToBytes(md5hex);

    const h1 = (hashBytes[12] & 0x0f) << 8;
    const h2 = hashBytes[13];
    const h = h1 | h2;
    const s = hashBytes[14];
    const l = hashBytes[15];

    const mapRange = (value, vmin, vmax, dmin, dmax) => {
      return ((value - vmin) * (dmax - dmin)) / (vmax - vmin) + dmin;
    };

    const hue = mapRange(h, 0, 4095, 0, 360);
    const sat = mapRange(s, 0, 255, 0, 20);
    const lum = mapRange(l, 0, 255, 0, 20);

    const finalHue = hue;
    const finalSat = 65.0 - sat;
    const finalLum = 75.0 - lum;

    const hNorm = finalHue / 360.0;
    const sNorm = finalSat / 100.0;
    const lNorm = finalLum / 100.0;

    const q = lNorm <= 0.5 ? lNorm * (sNorm + 1.0) : lNorm + sNorm - lNorm * sNorm;
    const p = 2.0 * lNorm - q;

    const hueToRgb = (a, b, h) => {
      if (h < 0.0) h += 1.0;
      else if (h > 1.0) h -= 1.0;

      if (h < 1.0 / 6.0) return a + (b - a) * 6.0 * h;
      if (h < 1.0 / 2.0) return b;
      if (h < 2.0 / 3.0) return a + (b - a) * (2.0 / 3.0 - h) * 6.0;
      return a;
    };

    const r = hueToRgb(p, q, hNorm + 1.0 / 3.0);
    const g = hueToRgb(p, q, hNorm);
    const b = hueToRgb(p, q, hNorm - 1.0 / 3.0);

    const color = `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`;
    const bgColor = "rgb(240, 240, 240)";

    ctx.fillStyle = bgColor;
    ctx.fillRect(0, 0, size, size);

    ctx.fillStyle = color;

    let byteIndex = 0;
    let hasLow = false;
    let lowNibble = 0;

    const nextNibble = () => {
      if (hasLow) {
        hasLow = false;
        return lowNibble;
      }

      if (byteIndex >= 16) return 0;

      const byte = hashBytes[byteIndex++];
      const high = (byte >> 4) & 0x0f;
      lowNibble = byte & 0x0f;
      hasLow = true;
      return high;
    };

    const grid = new Array(25).fill(false);

    for (let col = 2; col >= 0; --col) {
      for (let row = 0; row < 5; ++row) {
        if (byteIndex >= 16 && !hasLow) break;

        const nibble = nextNibble();
        const paint = nibble % 2 === 0;

        const ix = col + (row * 5);
        const mirrorCol = 4 - col;
        const mirrorIx = mirrorCol + (row * 5);

        grid[ix] = paint;
        grid[mirrorIx] = paint;
      }
    }

    for (let row = 0; row < GRID_SIZE; row++) {
      for (let col = 0; col < GRID_SIZE; col++) {
        if (grid[row * GRID_SIZE + col]) {
          const x = col * SQUARE_SIZE + MARGIN;
          const y = row * SQUARE_SIZE + MARGIN;

          ctx.fillRect(x, y, SQUARE_SIZE, SQUARE_SIZE);
        }
      }
    }

    return canvas.toDataURL();
  }

  function getIdentifier(img) {
    const url = img.src || "";
    const userIdMatch = url.match(/\/(\d+)(?:\?|$)/);
    if (userIdMatch) return userIdMatch[1];

    const link = img.closest('a[href*="/"]');
    if (link) {
      const href = link.getAttribute("href");
      const userMatch = href.match(/^\/([^\/]+)/);
      if (userMatch) return userMatch[1];
    }

    const card = img.closest("[data-hovercard-url]");
    if (card) {
      const hovercard = card.getAttribute("data-hovercard-url");
      const userMatch = hovercard.match(/\/users\/([^\/]+)/);
      if (userMatch) return userMatch[1];
    }

    return "error";
  }

  function replaceAllAvatars() {
    const images = document.querySelectorAll("img");
    let replacedCount = 0;

    images.forEach((img) => {
      const src = img.src || "";

      if (CONFIG.enabled) {
        if (!isAvatarUrl(src)) return;

        if (!img.dataset.originalSrc) {
          img.dataset.originalSrc = src;
        }
        const identifier = getIdentifier(img);
        const identicon = generateIdenticon(identifier, 420);
        if (identicon) {
          img.src = identicon;
          replacedCount++;
        }
      } else {
        if (img.dataset.originalSrc) {
          img.src = img.dataset.originalSrc;
          delete img.dataset.originalSrc;
          replacedCount++;
        }
      }
    });

    console.log(
      `Identicons: ${CONFIG.enabled ? "ON" : "OFF"
      }, Processed ${replacedCount} avatars`
    );
  }

  function initObserver() {
    const observer = new MutationObserver(function (mutations) {
      let shouldUpdate = false;
      for (const mutation of mutations) {
        if (mutation.type === "childList") {
          for (const node of mutation.addedNodes) {
            if (node.nodeType === 1) {
              if (node.tagName === "IMG" && isAvatarUrl(node.src || "")) {
                shouldUpdate = true;
                break;
              }
              if (node.querySelector && node.querySelector("img")) {
                const imgs = node.querySelectorAll("img");
                for (const img of imgs) {
                  if (isAvatarUrl(img.src || "")) {
                    shouldUpdate = true;
                    break;
                  }
                }
              }
            }
          }
        }
      }
      if (shouldUpdate) {
        setTimeout(replaceAllAvatars, 100);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  }

  function init() {
    registerMenuCommand();
    initObserver();
    replaceAllAvatars();

    let lastUrl = location.href;
    const checkUrlChange = setInterval(() => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        setTimeout(replaceAllAvatars, 500);
      }
    }, 1000);

    window.addEventListener("beforeunload", () => {
      clearInterval(checkUrlChange);
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();