Greasy Fork

Greasy Fork is available in English.

二维码自动解析

鼠标悬停时自动在本地解析二维码

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         二维码自动解析
// @description  鼠标悬停时自动在本地解析二维码
// @namespace    http://tampermonkey.net/
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.min.js
// @require      https://unpkg.com/@zxing/library@latest/umd/index.min.js
// @match        *://*/*
// @grant        GM_setClipboard
// @grant        GM_openInTab
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-start
// @version      2.8
// @author       Gemini
// @license      GPLv3
// @icon      data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABmJLR0QA/wD/AP+gvaeTAAABZElEQVR4nO3cQUrEMBiAURXv5QFceVJXHsCT6VYGpA5J+6X2vbVI4OPvZNJpHx4AAICredz6g5e3j68jFvKbz/fXzTX+dLb1Pu21EP5GgJgAMQFiAsQEiAkQex79B/fue28dvW9fbb0mICZATIDY8GfAra1r5Og1eLZ6vSYgJkBMgJgAMQFiAsQEiE3/HrDaPn9LvV4TEBMgJkBs+DOg/h3OvVZbrwmICRATICZATICYADEBYrufg4zecx3dt89+vmD22ZEJiAkQEyAmQEyAmAAxAWLDe9p6nz/b6Ho9J3wyAsQEiG3eEx69Rq92jV+NCYgJEBMgdvr3BdXPeI0yATEBYgLELveuiL3PrpwFncz0J2Rmq3dhezMBscu/K6JmAmICxASILb8Lmm21syUTEFt+As72q4p7mYDY5d8VUTMBMQFiAsSWvye85ehnwNwP+GcEiAkQEyAmQEyAmAAAAAAAcJBvjUVu7tMNP9IAAAAASUVORK5CYII=
// ==/UserScript==

(function() {
    'use strict';

    // === 配置 ===
    const DELAY_MS = 500;
    const TOLERANCE = 2;
    const CROP_TARGET_SIZE = 500; // 框选解析的最大尺寸 (超过此尺寸才缩小)
    const AUTO_SCAN_MAX_SIZE = 2000; // 超过此尺寸不自动解析

    // === ZXing 初始化 ===
    let zxingReaderStrict = null; // 仅用于悬停 (只识二维码)
    let zxingReaderAll = null;    // 用于强制解析 (识别所有)

    function getZXingReader(isForce) {
        if (!window.ZXing) return null;

        if (isForce) {
            // --- 模式 B: 强制解析 (全格式) ---
            if (!zxingReaderAll) {
                const hints = new Map();
                // 不设置 POSSIBLE_FORMATS 默认识别所有格式 (EAN, Code128, QR等)
                hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
                zxingReaderAll = new ZXing.BrowserMultiFormatReader(hints);
            }
            return zxingReaderAll;
        } else {
            // --- 模式 A: 悬停自动解析 (仅二维码) ---
            if (!zxingReaderStrict) {
                const hints = new Map();
                // 显式限制只识别 QR Code 和 Data Matrix
                const formats = [ZXing.BarcodeFormat.QR_CODE, ZXing.BarcodeFormat.DATA_MATRIX];
                hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, formats);
                // 悬停时也可以开启深度扫描 或者为了性能设为 false (这里建议开启以保证识别率)
                hints.set(ZXing.DecodeHintType.TRY_HARDER, true);
                zxingReaderStrict = new ZXing.BrowserMultiFormatReader(hints);
            }
            return zxingReaderStrict;
        }
    }

    // === 全局变量 ===
    let hoverTimer = null;
    let tooltip = null;
    let currentTarget = null;

    // 坐标相关
    let lastMouseScreenX = 0;
    let lastMouseScreenY = 0;
    let lastMouseClientX = 0;
    let lastMouseClientY = 0;
    let topWinOffset = null;

    // 组合键状态控制
    let isRightClickHolding = false;
    let leftClickCount = 0;
    let interactionTarget = null;
    let suppressContextMenu = false;
    let suppressClick = false;

    // 框选相关
    let isCropping = false;
    let isNoScaleCrop = false;
    let cropOverlay = null;
    let cropBox = null;
    let cropStart = { x: 0, y: 0 };
    let cropTarget = null;

    // 会话缓存
    const qrCache = new Map();
    const canvasCache = new WeakMap();

    const isTop = window.self === window.top;

    // === 样式注入 ===
    GM_addStyle(`
        #qr-custom-tooltip {
            position: fixed;
            z-index: 2147483647;
            background: rgba(0, 0, 0, 0.9);
            color: #fff;
            padding: 8px 12px;
            font-size: 12px;
            max-width: 320px;
            word-break: break-all;
            pointer-events: none;
            display: none;
            border: 1px solid #555;
            border-radius: 0px !important;
            box-shadow: none !important;
            line-height: 1.5;
            text-align: left;
        }
        .qr-detected-style {
            cursor: pointer !important;
            outline: none !important;
        }
        /* 框选遮罩 */
        #qr-crop-overlay {
            position: fixed;
            top: 0; left: 0; width: 100vw; height: 100vh;
            background: rgba(0, 0, 0, 0.3);
            z-index: 2147483646;
            cursor: crosshair;
            display: none;
        }
        #qr-crop-box {
            position: absolute;
            border: 2px solid #4CAF50;
            background: rgba(76, 175, 80, 0.2);
            pointer-events: none;
            display: none;
        }
    `);

    // ==========================================
    //      通信模块 (跨域支持)
    // ==========================================

    function sendToTop(type, payload = {}) {
        if (isTop) {
            handleMessage({ data: { type, payload } });
        } else {
            window.top.postMessage({ type: 'QR_SCRIPT_MSG', action: type, payload }, '*');
        }
    }

    if (isTop) {
        window.addEventListener('message', (event) => {
            if (event.data && event.data.type === 'QR_SCRIPT_MSG') {
                handleMessage({ data: { type: event.data.action, payload: event.data.payload } });
            }
        });
    }

    function handleMessage(e) {
        const { type, payload } = e.data;
        switch (type) {
            case 'SHOW_TOOLTIP':
                renderTooltip(payload.text, payload.coords, payload.isLink, payload.method);
                break;
            case 'HIDE_TOOLTIP':
                hideTooltipDOM();
                break;
            case 'SHOW_FEEDBACK':
                showFeedbackDOM();
                break;
        }
    }

    // ==========================================
    //      UI 渲染模块 (仅顶层窗口)
    // ==========================================

    function getTooltip() {
        if (!tooltip) {
            tooltip = document.createElement('div');
            tooltip.id = 'qr-custom-tooltip';
            document.body.appendChild(tooltip);
        }
        return tooltip;
    }

    function renderTooltip(text, coords, isLink, method) {
        const tip = getTooltip();
        const contentColor = isLink ? '#4dabf7' : '#ffffff';
        const actionColor = '#4CAF50';
        const bracketColor = '#F6B64E';
        const parenColor = '#B28BF7';

        const isLoading = text.startsWith('⌛');
        const isError = text.startsWith('❌');

        // 构建标题 HTML
        let titleHtml = '';
        if (method === '远程解析') {
            titleHtml = `<div style="margin-bottom:4px;">
                <span style="color:${bracketColor}; font-weight:bold;">[远程解析]</span>
            </div>`;
        } else {
            // 本地解析
            titleHtml = `<div style="margin-bottom:4px;">
                <span style="color:${bracketColor}; font-weight:bold;">[本地解析]</span>
                <span style="color:${parenColor}; font-weight:bold;"> (${escapeHtml(method || '未知')})</span>
            </div>`;
        }

        let htmlContent = '';
        if (isLoading) {
            htmlContent = `<div style="color:#FFD700; font-weight:bold;">${escapeHtml(text)}</div>`;
        } else if (isError) {
            htmlContent = `<div style="color:#FF5252; font-weight:bold;">${escapeHtml(text)}</div>`;
        } else {
            htmlContent = `
                ${titleHtml}
                <div style="color:${contentColor}; margin-bottom:6px;">${escapeHtml(text)}</div>
                <div style="color:${actionColor}; font-weight:bold; border-top:1px solid #444; padding-top:4px;">
                    ${isLink ? '🔗 点击打开链接' : '📋 点击复制文本'}
                </div>
            `;
        }

        tip.innerHTML = htmlContent;
        tip.style.display = 'block';

        // --- 坐标计算 ---
        let offsetY, offsetX;
        if (topWinOffset) {
            offsetX = topWinOffset.x;
            offsetY = topWinOffset.y;
        } else {
            const winScreenX = window.screenX !== undefined ? window.screenX : window.screenLeft;
            const winScreenY = window.screenY !== undefined ? window.screenY : window.screenTop;
            offsetX = winScreenX + (window.outerWidth - window.innerWidth);
            offsetY = winScreenY + (window.outerHeight - window.innerHeight);
        }

        let left = coords.absLeft - offsetX;
        let top = coords.absBottom - offsetY + 10;

        const tipRect = tip.getBoundingClientRect();
        const winHeight = window.innerHeight;
        const winWidth = window.innerWidth;

        if (top + tipRect.height > winHeight) {
            top = (coords.absTop - offsetY) - tipRect.height - 10;
        }
        if (left + tipRect.width > winWidth) left = winWidth - tipRect.width - 10;
        if (left < 0) left = 10;

        tip.style.top = top + 'px';
        tip.style.left = left + 'px';
    }

    function hideTooltipDOM() {
        if (tooltip) tooltip.style.display = 'none';
    }

    function showFeedbackDOM() {
        const tip = getTooltip();
        if (tip.style.display === 'none') return;
        const originalHTML = tip.innerHTML;
        tip.innerHTML = `<div style="font-size:14px; text-align:center; color:#4dabf7; font-weight:bold;">✅ 已复制到剪贴板</div>`;
        setTimeout(() => {
            if (tip.style.display !== 'none') tip.innerHTML = originalHTML;
        }, 1000);
    }

    // ==========================================
    //      逻辑处理模块 (所有 Frame)
    // ==========================================

    function requestShowTooltip(text, element, method = "JSQR") {
        if (currentTarget !== element) currentTarget = element;

        const isLink = isUrl(text);
        const rect = element.getBoundingClientRect();

        const frameOffsetX = (lastMouseScreenX && lastMouseClientX) ? (lastMouseScreenX - lastMouseClientX) : 0;
        const frameOffsetY = (lastMouseScreenY && lastMouseClientY) ? (lastMouseScreenY - lastMouseClientY) : 0;

        const coords = {
            absLeft: rect.left + frameOffsetX,
            absTop: rect.top + frameOffsetY,
            absBottom: rect.bottom + frameOffsetY
        };

        sendToTop('SHOW_TOOLTIP', { text, coords, isLink, method });
    }

    function requestHideTooltip() {
        currentTarget = null;
        sendToTop('HIDE_TOOLTIP');
    }

    function requestFeedback() {
        sendToTop('SHOW_FEEDBACK');
    }

    // ==========================================
    //      框选逻辑
    // ==========================================

    function startCropMode(target, noScale = false) {
        if (isCropping) return;
        isCropping = true;
        isNoScaleCrop = noScale;
        cropTarget = target;

        if (!cropOverlay) {
            cropOverlay = document.createElement('div');
            cropOverlay.id = 'qr-crop-overlay';
            cropBox = document.createElement('div');
            cropBox.id = 'qr-crop-box';
            cropOverlay.appendChild(cropBox);
            document.body.appendChild(cropOverlay);

            // 辅助函数
            const clamp = (val, min, max) => Math.min(Math.max(val, min), max);

            // 右键取消
            cropOverlay.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                e.stopPropagation();
                endCropMode();
                // 使用全局 cropTarget
                if (cropTarget) requestShowTooltip("❌ 已取消框选", cropTarget);
                return false;
            });

            // 鼠标按下开始框选
            cropOverlay.addEventListener('mousedown', (e) => {
                if (e.button === 2 || !cropTarget) return;

                // 关键修复 2: 在点击瞬间动态获取当前目标的 Rect
                // 确保获取的是当前 cropTarget 的位置 而不是第一次初始化时的位置
                const imgRect = cropTarget.getBoundingClientRect();

                // 限制起点坐标
                const startX = clamp(e.clientX, imgRect.left, imgRect.right);
                const startY = clamp(e.clientY, imgRect.top, imgRect.bottom);

                cropStart = { x: startX, y: startY };

                cropBox.style.left = startX + 'px';
                cropBox.style.top = startY + 'px';
                cropBox.style.width = '0px';
                cropBox.style.height = '0px';
                cropBox.style.display = 'block';

                const onMove = (ev) => {
                    // 限制终点坐标
                    const curX = clamp(ev.clientX, imgRect.left, imgRect.right);
                    const curY = clamp(ev.clientY, imgRect.top, imgRect.bottom);

                    const width = Math.abs(curX - cropStart.x);
                    const height = Math.abs(curY - cropStart.y);
                    const left = Math.min(curX, cropStart.x);
                    const top = Math.min(curY, cropStart.y);

                    cropBox.style.width = width + 'px';
                    cropBox.style.height = height + 'px';
                    cropBox.style.left = left + 'px';
                    cropBox.style.top = top + 'px';
                };

                const onUp = (ev) => {
                    window.removeEventListener('mousemove', onMove);
                    window.removeEventListener('mouseup', onUp);

                    if (ev.button !== 0 || !isCropping) return;

                    const rect = cropBox.getBoundingClientRect();
                    endCropMode();

                    if (rect.width < 5 || rect.height < 5) return;

                    // 关键修复 3: 将当前的 cropTarget 传递给处理函数
                    processCropScan(cropTarget, rect);
                };

                window.addEventListener('mousemove', onMove);
                window.addEventListener('mouseup', onUp);
            });
        }

        cropOverlay.style.display = 'block';
        const tipText = noScale ? "⌛ 原图框选" : "⌛ 缩放框选";
        requestShowTooltip(tipText, target);
    }

    function endCropMode() {
        isCropping = false;
        if (cropOverlay) cropOverlay.style.display = 'none';
        if (cropBox) cropBox.style.display = 'none';
    }

    function processCropScan(target, selectionRect) {
        const targetRect = target.getBoundingClientRect();
        const selX = selectionRect.left;
        const selY = selectionRect.top;
        const selW = selectionRect.width;
        const selH = selectionRect.height;
        const imgX = targetRect.left;
        const imgY = targetRect.top;
        const relX = selX - imgX;
        const relY = selY - imgY;

        const cropRect = {
            x: relX,
            y: relY,
            w: selW,
            h: selH,
            noScale: isNoScaleCrop
        };

        scanElement(target, true, cropRect);
    }

    // === 统一入口 ===
    function scanElement(target, force = false, cropRect = null) {
        // 获取当前缓存状态 (用于强制解析时的判断)
        let prevCache = null;
        if (target.tagName === 'IMG' && target.src) prevCache = qrCache.get(target.src);
        else if (target.tagName === 'CANVAS') prevCache = canvasCache.get(target);

        if (target.tagName === 'IMG') {
            scanImage(target, force, cropRect, prevCache);
        } else if (target.tagName === 'CANVAS') {
            scanCanvas(target, force, cropRect, prevCache);
        }
    }

    // === 远程解析 ===
    function scanExternal(target) {
        if (target.tagName !== 'IMG' || !target.src || !/^http/.test(target.src)) {
            requestShowTooltip("❌ 远程解析仅支持 http/https 图片链接", target);
            return;
        }
        const src = target.src;
        requestShowTooltip("⌛ 正在连接远程服务器解析...", target);

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://zxing.org/w/decode?u=" + encodeURIComponent(src),
            onload: function(response) {
                if (response.status === 200) {
                    const parser = new DOMParser();
                    const doc = parser.parseFromString(response.responseText, "text/html");
                    const tds = doc.querySelectorAll('td');
                    let resultText = null;
                    for (let i = 0; i < tds.length; i++) {
                        if (tds[i].textContent.trim() === "Parsed Result") {
                            const nextTd = tds[i].nextElementSibling;
                            if (nextTd) {
                                const pre = nextTd.querySelector('pre');
                                if (pre) { resultText = pre.textContent; break; }
                            }
                        }
                    }
                    if (resultText) {
                        qrCache.set(src, { status: 'success', text: resultText, method: "远程解析" });
                        applyQrSuccess(target, resultText, "远程解析");
                    } else {
                        requestShowTooltip("❌ 远程解析失败", target);
                    }
                } else {
                    requestShowTooltip("❌ 远程服务器响应错误: " + response.status, target);
                }
            },
            onerror: function() {
                requestShowTooltip("❌ 网络请求失败", target);
            }
        });
    }

    // ==========================================
    //      图像获取与预处理
    // ==========================================

    function scanImage(img, force, cropRect, prevCache) {
        const src = img.src;
        if (!src) return;
        // 如果非强制且已有缓存(且非skipped) 则跳过
        // 注意:如果是 skipped (too_large) force 模式下应该允许继续
        if (!force && !cropRect && qrCache.has(src)) return;

        let displayWidth = img.width || img.clientWidth || 0;
        let displayHeight = img.height || img.clientHeight || 0;

        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        const tempImg = new Image();
        tempImg.crossOrigin = "Anonymous";
        tempImg.src = src;

        tempImg.onload = () => processImage(tempImg, canvas, context, img, src, force, 'IMG', displayWidth, displayHeight, cropRect, prevCache);
        tempImg.onerror = () => scanImage_Fallback(img, src, force, displayWidth, displayHeight, cropRect);
    }

    function scanImage_Fallback(originalImg, src, force, w, h, cropRect) {
        GM_xmlhttpRequest({
            method: "GET",
            url: src,
            responseType: "blob",
            onload: function(response) {
                if (response.status === 200) {
                    const blob = response.response;
                    const blobUrl = URL.createObjectURL(blob);
                    const tempImg = new Image();
                    tempImg.onload = () => {
                        const canvas = document.createElement('canvas');
                        const context = canvas.getContext('2d');
                        processImage(tempImg, canvas, context, originalImg, src, force, 'IMG', w, h, cropRect);
                        URL.revokeObjectURL(blobUrl);
                    };
                    tempImg.onerror = () => {
                        if (!cropRect) qrCache.set(src, null);
                        URL.revokeObjectURL(blobUrl);
                    };
                    tempImg.src = blobUrl;
                } else {
                    if (!cropRect) qrCache.set(src, null);
                }
            },
            onerror: () => { if (!cropRect) qrCache.set(src, null); }
        });
    }

    function scanCanvas(canvasEl, force, cropRect, prevCache) {
        if (!force && !cropRect && canvasCache.has(canvasEl)) return;

        try {
            let context = canvasEl.getContext('2d');
            if (context) {
                try {
                    const imageData = context.getImageData(0, 0, canvasEl.width, canvasEl.height);

                    // 1. 确定源尺寸
                    const sourceW = canvasEl.width;
                    const sourceH = canvasEl.height;

                    // 2. 计算裁剪
                    let drawX = 0, drawY = 0, drawW = sourceW, drawH = sourceH;
                    if (cropRect) {
                        const clientW = canvasEl.clientWidth || sourceW;
                        const clientH = canvasEl.clientHeight || sourceH;
                        const scaleX = sourceW / clientW;
                        const scaleY = sourceH / clientH;

                        drawX = cropRect.x * scaleX;
                        drawY = cropRect.y * scaleY;
                        drawW = cropRect.w * scaleX;
                        drawH = cropRect.h * scaleY;
                    }

                    // 3. 计算缩放 (仅缩小 不放大)
                    let targetW = drawW;
                    let targetH = drawH;
                    if (cropRect) {
                        const maxDim = Math.max(drawW, drawH);
                        // 只有当尺寸超过目标尺寸时才缩放
                        if (maxDim > CROP_TARGET_SIZE) {
                            const scale = CROP_TARGET_SIZE / maxDim;
                            targetW = drawW * scale;
                            targetH = drawH * scale;
                        }
                    }

                    // 4. 绘制到新 Canvas (加白边)
                    const padding = 50;
                    const finalCanvas = document.createElement('canvas');
                    finalCanvas.width = targetW + (padding * 2);
                    finalCanvas.height = targetH + (padding * 2);
                    const finalCtx = finalCanvas.getContext('2d');
                    finalCtx.fillStyle = '#FFFFFF';
                    finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);

                    // 绘制并缩放
                    finalCtx.drawImage(canvasEl, drawX, drawY, drawW, drawH, padding, padding, targetW, targetH);

                    runScanPipeline(finalCanvas, finalCtx, canvasEl, force, 'CANVAS', canvasEl, !!cropRect, prevCache);
                } catch (e) {
                    canvasCache.set(canvasEl, null);
                }
            } else {
                const dataUrl = canvasEl.toDataURL();
                const tempImg = new Image();
                tempImg.onload = () => {
                    const tempCanvas = document.createElement('canvas');
                    const tempCtx = tempCanvas.getContext('2d');
                    processImage(tempImg, tempCanvas, tempCtx, canvasEl, null, force, 'CANVAS', canvasEl.width, canvasEl.height, cropRect);
                };
                tempImg.src = dataUrl;
            }
        } catch (e) {
            canvasCache.set(canvasEl, null);
        }
    }

    // === 高质量缩放辅助函数 (模拟 Lanczos 效果) ===
    function smartDownscale(imageObj, ctx, sourceX, sourceY, sourceW, sourceH, targetX, targetY, targetW, targetH) {
        // 1. 开启浏览器最高质量插值
        ctx.imageSmoothingEnabled = true;
        ctx.imageSmoothingQuality = 'high';

        // 2. 如果缩放比例小于 2 倍 直接绘制 (分步缩放收益不大)
        if (sourceW <= targetW * 2 && sourceH <= targetH * 2) {
            ctx.drawImage(imageObj, sourceX, sourceY, sourceW, sourceH, targetX, targetY, targetW, targetH);
            return;
        }

        // 3. 分步缩放逻辑
        // 创建临时 Canvas 进行中间态处理
        let tempCanvas = document.createElement('canvas');
        let tempCtx = tempCanvas.getContext('2d');
        let curW = sourceW;
        let curH = sourceH;

        tempCanvas.width = curW;
        tempCanvas.height = curH;

        // 第一步:裁剪原图到临时 Canvas
        tempCtx.drawImage(imageObj, sourceX, sourceY, sourceW, sourceH, 0, 0, curW, curH);

        // 循环减半缩放 直到接近目标尺寸
        while (curW > targetW * 2) {
            const newW = Math.floor(curW * 0.5);
            const newH = Math.floor(curH * 0.5);

            // 创建更小的临时 Canvas
            let nextCanvas = document.createElement('canvas');
            nextCanvas.width = newW;
            nextCanvas.height = newH;
            let nextCtx = nextCanvas.getContext('2d');

            // 绘制缩小版
            nextCtx.drawImage(tempCanvas, 0, 0, curW, curH, 0, 0, newW, newH);

            // 更新引用
            curW = newW;
            curH = newH;
            tempCanvas = nextCanvas; // 丢弃旧的大 Canvas
        }

        // 4. 最后一步:绘制到目标 Canvas
        ctx.drawImage(tempCanvas, 0, 0, curW, curH, targetX, targetY, targetW, targetH);
    }

    function processImage(imageObj, canvas, context, targetEl, cacheKey, force, type, displayWidth, displayHeight, cropRect, prevCache) {
        // 1. 获取原始尺寸
        let naturalW = imageObj.naturalWidth;
        let naturalH = imageObj.naturalHeight;

        // SVG 检测
        const isSVG = /\.svg($|\?|#)/i.test(imageObj.src) || /^data:image\/svg/i.test(imageObj.src);
        const isUnknownSize = !naturalW || naturalW === 0;

        // 标记:是否必须使用 5 参数模式 (SVG 或 无尺寸图片)
        const forceSimpleMode = isSVG || isUnknownSize;

        // 如果没有原始尺寸(通常是某些 SVG) 才使用显示尺寸兜底
        // 如果 SVG 有原始尺寸(如 width="1000") 则保留原始尺寸以获得更高清晰度
        if (isUnknownSize) {
            naturalW = displayWidth || 300;
            naturalH = displayHeight || 300;
        }

        // 2. 计算目标尺寸
        let targetW = naturalW;
        let targetH = naturalH;

        // 仅在框选模式下计算裁剪尺寸
        if (cropRect && !forceSimpleMode) {
            const scaleX = naturalW / displayWidth;
            const scaleY = naturalH / displayHeight;
            targetW = cropRect.w * scaleX;
            targetH = cropRect.h * scaleY;
        }

        // === 关键修改:缩放限制逻辑 ===
        // 只有在【框选模式】下才执行缩小 (为了性能和聚焦)
        // 【全图模式】下始终保持 1:1 原始分辨率 (为了最高识别率)
        if (cropRect) {
            // 如果 cropRect.noScale 为 true 则跳过缩小逻辑
            if (!cropRect.noScale) {
                const maxDim = Math.max(targetW, targetH);
                if (maxDim > CROP_TARGET_SIZE) {
                    const scale = CROP_TARGET_SIZE / maxDim;
                    targetW *= scale;
                    targetH *= scale;
                }
            }
        }

        const padding = 50;
        canvas.width = targetW + (padding * 2);
        canvas.height = targetH + (padding * 2);

        // 3. 绘制背景
        context.fillStyle = '#FFFFFF';
        context.fillRect(0, 0, canvas.width, canvas.height);

        // 4. 绘制图像
        if (cropRect && !forceSimpleMode) {
            // 【模式 A:位图裁剪】(仅框选且非SVG)
            const scaleX = naturalW / displayWidth;
            const scaleY = naturalH / displayHeight;

            let sourceX = cropRect.x * scaleX;
            let sourceY = cropRect.y * scaleY;
            let sourceW = cropRect.w * scaleX;
            let sourceH = cropRect.h * scaleY;

            // 边界保护
            if (sourceX < 0) sourceX = 0;
            if (sourceY < 0) sourceY = 0;
            if (sourceX + sourceW > naturalW) sourceW = naturalW - sourceX;
            if (sourceY + sourceH > naturalH) sourceH = naturalH - sourceY;

            // 框选模式下 targetW 已经被限制在 500px 以内 smartDownscale 会自动处理缩放
            smartDownscale(imageObj, context, sourceX, sourceY, sourceW, sourceH, padding, padding, targetW, targetH);

        } else {
            // 【模式 B:全图模式】(SVG 或 全图位图)
            if (forceSimpleMode) {
                // SVG: 浏览器原生绘制 (矢量无损)
                context.imageSmoothingEnabled = true;
                context.imageSmoothingQuality = 'high';
                context.drawImage(imageObj, padding, padding, targetW, targetH);
            } else {
                // 位图全图:
                // 因为移除了尺寸限制 targetW 等于 naturalW
                // smartDownscale 内部检测到源尺寸和目标尺寸一致时 会直接绘制 不会产生性能损耗
                smartDownscale(imageObj, context, 0, 0, naturalW, naturalH, padding, padding, targetW, targetH);
            }
        }

        runScanPipeline(canvas, context, targetEl, force, type, cacheKey, !!cropRect, prevCache);
    }

    // ==========================================
    //      核心扫描管道 (JSQR + ZXing)
    // ==========================================

    // 让浏览器有机会渲染一帧 (避免 UI 假死)
    function yieldToMain() {
        return new Promise(resolve => setTimeout(resolve, 0));
    }

    async function runScanPipeline(canvas, context, targetEl, force, type, cacheKey, isCrop, prevCache) {
        if (force) requestShowTooltip("⌛ 正在进行强制解析...", targetEl);

        await yieldToMain();

        let result = null;
        const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        const suffix = isCrop ? " 框选" : "";

        // === 智能跳过逻辑 ===
        // 如果是强制解析 且之前的失败原因是 "standard_failed" (标准解析已尝试过且失败)
        // 则直接跳过 Phase 1 进入 Phase 2
        let skipStandard = false;
        if (force && prevCache && prevCache.status === 'failed' && prevCache.reason === 'standard_failed') {
            skipStandard = true;
            requestShowTooltip("⌛ 深度解析...", targetEl);
            await yieldToMain();
        }

        // --- 阶段 1: 标准解析 ---
        if (!skipStandard) {
            // 1.1 JSQR 标准
            result = jsQR(imageData.data, imageData.width, imageData.height);
            if (result) {
                handleSuccess(result.data, "JSQR" + suffix, type, cacheKey, targetEl);
                return;
            }

            await yieldToMain();

            // 1.2 ZXing 标准
            result = await tryZXing(canvas, force);
            if (result) {
                handleSuccess(result, "ZXing" + suffix, type, cacheKey, targetEl);
                return;
            }
        }

        // 如果不是强制模式 且标准解析失败 则记录失败原因并退出
        if (!force) {
            handleFail(type, cacheKey, targetEl, false, "standard_failed"); // <--- 记录原因
            return;
        }

        // --- 阶段 2: 增强解析 (仅强制模式) ---

        requestShowTooltip("⌛ 正在尝试反色解析...", targetEl);
        await yieldToMain();

        // 反色数据准备
        const invertedData = new Uint8ClampedArray(imageData.data);
        for (let i = 0; i < invertedData.length; i += 4) {
            invertedData[i] = 255 - invertedData[i];
            invertedData[i + 1] = 255 - invertedData[i + 1];
            invertedData[i + 2] = 255 - invertedData[i + 2];
            invertedData[i + 3] = 255;
        }

        // 2.1 JSQR 反色
        result = jsQR(invertedData, imageData.width, imageData.height);
        if (result) {
            handleSuccess(result.data, "JSQR 反色" + suffix, type, cacheKey, targetEl);
            return;
        }

        await yieldToMain();

        // 2.2 ZXing 反色
        const invertedImageData = new ImageData(invertedData, canvas.width, canvas.height);
        context.putImageData(invertedImageData, 0, 0);
        result = await tryZXing(canvas, force);
        if (result) {
            handleSuccess(result, "ZXing 反色" + suffix, type, cacheKey, targetEl);
            return;
        }

        requestShowTooltip("⌛ 正在尝试二值化解析...", targetEl);
        await yieldToMain();

        // 二值化数据准备
        const binarizedData = new Uint8ClampedArray(imageData.data);
        const len = binarizedData.length;
        let totalLum = 0;
        for (let i = 0; i < len; i += 4) {
            totalLum += 0.299 * binarizedData[i] + 0.587 * binarizedData[i+1] + 0.114 * binarizedData[i+2];
        }
        const avgLum = totalLum / (len / 4);
        for (let i = 0; i < len; i += 4) {
            const lum = 0.299 * binarizedData[i] + 0.587 * binarizedData[i+1] + 0.114 * binarizedData[i+2];
            const val = lum > avgLum ? 255 : 0;
            binarizedData[i] = val;
            binarizedData[i+1] = val;
            binarizedData[i+2] = val;
            binarizedData[i+3] = 255;
        }

        // 2.3 JSQR 二值化
        result = jsQR(binarizedData, imageData.width, imageData.height);
        if (result) {
            handleSuccess(result.data, "JSQR 二值化" + suffix, type, cacheKey, targetEl);
            return;
        }

        await yieldToMain();

        // 2.4 ZXing 二值化
        const binarizedImageData = new ImageData(binarizedData, canvas.width, canvas.height);
        context.putImageData(binarizedImageData, 0, 0);
        result = await tryZXing(canvas, force);
        if (result) {
            handleSuccess(result, "ZXing 二值化" + suffix, type, cacheKey, targetEl);
            return;
        }

        handleFail(type, cacheKey, targetEl, true, "force_failed");
    }

    function tryZXing(canvas, isForce) {
        return new Promise((resolve) => {
            if (typeof ZXing === 'undefined') { resolve(null); return; }

            const dataUrl = canvas.toDataURL('image/png');
            const img = new Image();
            img.onload = () => {
                // 关键修改:将 isForce 传入获取对应的 Reader
                const reader = getZXingReader(isForce);
                if (!reader) { resolve(null); return; }

                reader.decodeFromImageElement(img)
                    .then(res => resolve(res.text))
                    .catch(() => resolve(null));
            };
            img.onerror = () => resolve(null);
            img.src = dataUrl;
        });
    }

    function handleSuccess(text, method, type, cacheKey, targetEl) {
        const cacheObj = { status: 'success', text: text, method: method };

        if (type === 'IMG') qrCache.set(cacheKey, cacheObj);
        else canvasCache.set(targetEl, cacheObj);

        applyQrSuccess(targetEl, text, method);
    }

    function handleFail(type, cacheKey, targetEl, isForce, reason = "unknown") {
        if (!isForce) {
            const failObj = { status: 'failed', reason: reason };

            if (type === 'IMG') qrCache.set(cacheKey, failObj);
            else canvasCache.set(targetEl, failObj);
        }

        if (isForce) {
            requestShowTooltip("❌ 强制解析失败", targetEl);
        }
    }

    // ==========================================
    //      公共辅助函数
    // ==========================================

    function applyQrSuccess(el, text, method) {
        if (!method.includes("框选")) {
            el.dataset.hasQr = "true";
            el.classList.add('qr-detected-style');
        }
        requestShowTooltip(text, el, method);
    }

    function isUrl(text) {
        if (!text) return false;
        // ^ : 开始
        // \s*: 允许开头有空格
        // https?:\/\/: 协议
        // [^\s]+: 链接主体不能包含空格
        // \s*: 允许结尾有空格
        // $ : 结束
        return /^\s*https?:\/\/[^\s]+\s*$/i.test(text);
    }
    function escapeHtml(text) {
        if (!text) return "";
        return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
    }

    // ==========================================
    //      事件监听
    // ==========================================

    document.addEventListener('mousemove', (e) => {
        lastMouseScreenX = e.screenX;
        lastMouseScreenY = e.screenY;
        lastMouseClientX = e.clientX;
        lastMouseClientY = e.clientY;

        if (isTop) {
            topWinOffset = {
                x: e.screenX - e.clientX,
                y: e.screenY - e.clientY
            };
        }
    }, true);

    document.addEventListener('mouseover', (e) => {
        if (isCropping) return;
        const target = e.target;
        const isImg = target.tagName === 'IMG';
        const isCanvas = target.tagName === 'CANVAS';

        if (!isImg && !isCanvas) return;
        if (isImg && (!target.complete || target.naturalWidth === 0)) return;

        // --- 1. 获取尺寸 ---
        let w, h;
        if (isImg) {
            w = target.naturalWidth;
            h = target.naturalHeight;
        } else {
            w = target.width || target.clientWidth;
            h = target.height || target.clientHeight;
        }

        // --- 2. 检查缓存 ---
        let cacheData = null;
        if (isImg && target.src) cacheData = qrCache.get(target.src);
        else if (isCanvas) cacheData = canvasCache.get(target);

        if (cacheData) {
            // 如果是成功状态 显示结果
            if (cacheData.status === 'success') {
                if (!target.dataset.hasQr) applyQrSuccess(target, cacheData.text, cacheData.method);
                else requestShowTooltip(cacheData.text, target, cacheData.method);
            }
            // 如果是失败或跳过状态 直接返回 不再重复尝试
            return;
        }

        // --- 3. 尺寸检查 (新增逻辑) ---
        // 如果尺寸超过 2000 且没有缓存 则标记为因过大而跳过
        if (w > AUTO_SCAN_MAX_SIZE || h > AUTO_SCAN_MAX_SIZE) {
            const skipObj = { status: 'skipped', reason: 'too_large' };
            if (isImg && target.src) qrCache.set(target.src, skipObj);
            else if (isCanvas) canvasCache.set(target, skipObj);
            return; // 停止自动解析
        }

        if (Math.abs(w - h) > TOLERANCE || w < 30) {
            const failObj = { status: 'failed', reason: 'invalid_size' };
            if (isImg && target.src) qrCache.set(target.src, failObj);
            else if (isCanvas) canvasCache.set(target, failObj);
            return;
        }

        hoverTimer = setTimeout(() => {
            if (isCropping) return;
            // 再次检查缓存防止并发
            if (isImg && qrCache.has(target.src)) return;
            if (isCanvas && canvasCache.has(target)) return;
            scanElement(target, false);
        }, DELAY_MS);
    });

    document.addEventListener('mouseout', (e) => {
        const t = e.target;
        if (t.tagName === 'IMG' || t.tagName === 'CANVAS') {
            clearTimeout(hoverTimer);
            if (currentTarget === t && !isCropping) {
                requestHideTooltip();
            }
        }
    });

    // === 交互逻辑 ===

    document.addEventListener('mousedown', (e) => {
        if (isCropping) return;

        if (e.button === 2) {
            isRightClickHolding = true;
            leftClickCount = 0;
            interactionTarget = e.target;
            suppressContextMenu = false;
        }
        else if (e.button === 0) {
            if (isRightClickHolding) {
                if (interactionTarget && (interactionTarget.tagName === 'IMG' || interactionTarget.tagName === 'CANVAS')) {
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();

                    leftClickCount++;
                    suppressContextMenu = true;
                    suppressClick = true;
                }
            }
        }
    }, true);

    document.addEventListener('mouseup', (e) => {
        if (isCropping) return;

        if (e.button === 2) {
            isRightClickHolding = false;

            if (leftClickCount > 0 && interactionTarget) {
                // 1次点击 -> 强制本地解析 (全策略)
                if (leftClickCount === 1) {
                    scanElement(interactionTarget, true);
                }
                // 2次点击 -> 远程解析
                else if (leftClickCount === 2) {
                    scanExternal(interactionTarget);
                }
                // 3次点击 -> 普通框选 (会缩小到 500px)
                else if (leftClickCount === 3) {
                    startCropMode(interactionTarget, false);
                }
                // 4次点击 -> 原图框选 (不缩小)
                else if (leftClickCount === 4) {
                    startCropMode(interactionTarget, true);
                }
            }

            interactionTarget = null;
            leftClickCount = 0;
        }
    }, true);

    document.addEventListener('contextmenu', (e) => {
        if (suppressContextMenu) {
            e.preventDefault();
            e.stopPropagation();
            suppressContextMenu = false;
        }
    }, true);

    document.addEventListener('click', (e) => {
        if (suppressClick) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            suppressClick = false;
            return;
        }

        const target = e.target;
        if ((target.tagName === 'IMG' || target.tagName === 'CANVAS') && target.dataset.hasQr === "true") {
            let data = null;
            let cacheData = null;

            if (target.tagName === 'IMG') cacheData = qrCache.get(target.src);
            else cacheData = canvasCache.get(target);

            // 检查 status === 'success'
            if (cacheData && cacheData.status === 'success') {
                data = cacheData.text;
            }

            if (data) {
                e.preventDefault();
                e.stopPropagation();

                if (isUrl(data)) {
                    GM_openInTab(data, { active: true, insert: true });
                } else {
                    GM_setClipboard(data);
                    requestFeedback();
                }
            }
        }
    }, true);

    document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && isCropping) {
            endCropMode();
            requestShowTooltip("❌ 已取消框选", currentTarget || document.body);
        }
    });

})();