Northie / 河畔新旧版通用水印处理

// ==UserScript==
// @name         河畔新旧版通用水印处理
// @namespace    https://openuserjs.org/users/Northie
// @version      2026.08.26
// @description  新版河畔替换 SVG 水印 UID,旧版河畔隐藏 Canvas 生成的 PNG 水印
// @author       Northie
// @copyright    2026, Northie (https://openuserjs.org/users/Northie)
// @license      GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt
// @homepageURL  https://github.com/WhyPilotXia/Crack-SVG-watermark-for-bbs.uestc.edu.cn
// @supportURL   https://github.com/WhyPilotXia/Crack-SVG-watermark-for-bbs.uestc.edu.cn/issues
// @match        https://bbs.uestc.edu.cn/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

// ==OpenUserJS==
// @author       Northie
// ==/OpenUserJS==

(function () {
  'use strict'

  const OLD_TEXT = '9999' // 新版页面当前水印中的 UID,改成你自己的 UID
  const NEW_TEXT = '114514' // 新版页面希望显示的 UID
  const STYLE_ID = 'tm-qshp-universal-watermark-style'
  const SVG_BASE64_PATTERN = /data:image\/svg\+xml(?:;charset=[^;,]+)?;base64,([A-Za-z0-9+/=_-]+)/gi

  function decodeBase64Utf8(base64) {
    let normalized = base64.replace(/-/g, '+').replace(/_/g, '/')
    const remainder = normalized.length % 4
    if (remainder) normalized += '='.repeat(4 - remainder)

    const binary = atob(normalized)
    const bytes = Uint8Array.from(binary, char => char.charCodeAt(0))
    return new TextDecoder().decode(bytes)
  }

  function encodeBase64Utf8(text) {
    const bytes = new TextEncoder().encode(text)
    let binary = ''

    for (let index = 0; index < bytes.length; index += 0x8000) {
      binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000))
    }

    return btoa(binary)
  }

  function rewriteBackgroundImage(style) {
    if (!style || !OLD_TEXT || OLD_TEXT === NEW_TEXT) return false

    const backgroundImage = style.backgroundImage
    if (!backgroundImage || !backgroundImage.includes('data:image/svg+xml')) return false

    let rewritten = false
    const nextBackgroundImage = backgroundImage.replace(
      SVG_BASE64_PATTERN,
      (dataUrl, base64) => {
        try {
          const svg = decodeBase64Utf8(base64)
          if (!svg.includes(OLD_TEXT)) return dataUrl

          const modifiedSvg = svg.replaceAll(OLD_TEXT, NEW_TEXT)
          rewritten = true
          return dataUrl.slice(0, -base64.length) + encodeBase64Utf8(modifiedSvg)
        } catch (error) {
          console.warn('[QSHp watermark] SVG 水印解析失败', error)
          return dataUrl
        }
      }
    )

    if (!rewritten) return false

    style.backgroundImage = nextBackgroundImage
    console.info(`[QSHp watermark] 新版 SVG 水印已替换:${OLD_TEXT} → ${NEW_TEXT}`)
    return true
  }

  function processElement(element) {
    if (!(element instanceof Element)) return
    rewriteBackgroundImage(element.style)
  }

  function processNode(node) {
    if (!node) return

    if (node.nodeType === Node.ELEMENT_NODE) {
      processElement(node)
    }

    if (
      node.nodeType === Node.ELEMENT_NODE ||
      node.nodeType === Node.DOCUMENT_FRAGMENT_NODE
    ) {
      node
        .querySelectorAll?.('[style*="data:image/svg+xml"]')
        .forEach(processElement)
    }
  }

  function installInsertionHooks() {
    const rawAppendChild = Node.prototype.appendChild
    const rawInsertBefore = Node.prototype.insertBefore
    const rawReplaceChild = Node.prototype.replaceChild

    Node.prototype.appendChild = function (node) {
      processNode(node)
      return rawAppendChild.call(this, node)
    }

    Node.prototype.insertBefore = function (node, referenceNode) {
      processNode(node)
      return rawInsertBefore.call(this, node, referenceNode)
    }

    Node.prototype.replaceChild = function (newChild, oldChild) {
      processNode(newChild)
      return rawReplaceChild.call(this, newChild, oldChild)
    }

    for (const methodName of ['append', 'prepend', 'replaceChildren']) {
      const rawMethod = Element.prototype[methodName]
      if (typeof rawMethod !== 'function') continue

      Element.prototype[methodName] = function (...nodes) {
        nodes.forEach(node => {
          if (typeof node !== 'string') processNode(node)
        })
        return rawMethod.apply(this, nodes)
      }
    }

    const rawInsertAdjacentElement = Element.prototype.insertAdjacentElement
    if (typeof rawInsertAdjacentElement === 'function') {
      Element.prototype.insertAdjacentElement = function (position, element) {
        processNode(element)
        return rawInsertAdjacentElement.call(this, position, element)
      }
    }
  }

  function installLegacyPngRule() {
    if (!document.documentElement || document.getElementById(STYLE_ID)) return

    const style = document.createElement('style')
    style.id = STYLE_ID
    style.textContent = `
body > div[style*="data:image/png;base64"][style*="pointer-events"][style*="position"][style*="repeat"] {
  display: none !important;
  background: none !important;
}
`
    ;(document.head || document.documentElement).appendChild(style)
  }

  function initializeDocumentHandling() {
    if (!document.documentElement) return false

    installLegacyPngRule()
    processNode(document.documentElement)

    const observer = new MutationObserver(records => {
      installLegacyPngRule()

      for (const record of records) {
        record.addedNodes.forEach(processNode)
      }
    })

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

    return true
  }

  installInsertionHooks()

  if (!initializeDocumentHandling()) {
    const bootstrapObserver = new MutationObserver(() => {
      if (!initializeDocumentHandling()) return
      bootstrapObserver.disconnect()
    })

    bootstrapObserver.observe(document, { childList: true })
  }
})()