// ==UserScript==
// @name 识别二维码 QR Reader
// @name:en QR Reader - Decode QR Codes on Page
// @namespace Runas_Ron/userscripts/decode-qr-code
// @version 1.0.0
// @author Runas_Ron (forked from lovefqy)
// @description 右键图片或内联 SVG 即可识别二维码;多通道预处理(原生彩色、中值去噪、Sauvola 局部阈值);分位数对比度拉伸对抗压缩发灰;支持剪贴板截图识别与多尺度自适应扫描。
// @description:en Right-click any image or inline SVG to decode QR codes. Multi-channel preprocessing (native color, median filter, Sauvola local threshold) with quantile contrast stretching. Supports clipboard decoding and multi-scale scanning.
// @license MIT
// @match http*://*/*
// @connect *
// @require https://unpkg.com/jsqr@1.4.0/dist/jsQR.js
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
const jsQR = window.jsQR;
if (!jsQR) {
console.error('[decode-qr] jsQR 加载失败');
toast('jsQR 加载失败,请检查网络连接后刷新页面重试。');
}
const gmSetClipboard = (typeof GM_setClipboard !== 'undefined') ? GM_setClipboard : null;
const gmRegister = (typeof GM_registerMenuCommand !== 'undefined') ? GM_registerMenuCommand : null;
const gmXmlHttpRequest = (typeof GM_xmlhttpRequest !== 'undefined') ? GM_xmlhttpRequest : null;
const MEDIAN_FILTER_MAX_PX = 2_000_000;
const SAUVOLA_MAX_PX = 2_000_000;
let selDomRef = null;
function setSel(el) { selDomRef = el ? new WeakRef(el) : null; }
/* ======================== 基础工具 ======================== */
function safeBtoa(str) {
const bytes = new TextEncoder().encode(str);
let bin = '';
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function blobToImg(blob) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => { URL.revokeObjectURL(url); resolve(img); };
img.onerror = () => { URL.revokeObjectURL(url); reject(new Error('图片加载失败')); };
img.src = url;
});
}
function loadImageCors(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
function fetchImageBlob(url) {
return new Promise((resolve, reject) => {
if (gmXmlHttpRequest) {
gmXmlHttpRequest({
method: 'GET', url, responseType: 'blob', timeout: 10000, anonymous: true,
onload: r => r.status === 200 ? resolve(r.response) : reject(new Error('HTTP ' + r.status)),
onerror: () => reject(new Error('network error')),
ontimeout: () => reject(new Error('timeout')),
});
} else {
reject(new Error('GM_xmlhttpRequest unavailable'));
}
});
}
/* ======================== 错误处理与 UI ======================== */
const ERR = {
NO_TARGET: { title: '未找到有效图片', hint: '请先右键一张
图片或