Greasy Fork

Greasy Fork is available in English.

二维码自动解析

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

当前为 2025-12-02 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴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.3
// @author       Gemini
// @license      GPLv3
// ==/UserScript==

(function() {
    'use strict';

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

    // === 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 cropOverlay = null;
    let cropBox = null;
    let cropStart = { x: 0, y: 0 };

    // 会话缓存
    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) {
        if (isCropping) return;
        isCropping = true;

        // 1. 获取目标图片的边界信息 (相对于视口)
        const imgRect = target.getBoundingClientRect();

        // 辅助函数:将数值限制在 min 和 max 之间
        const clamp = (val, min, max) => Math.min(Math.max(val, min), max);

        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);

            cropOverlay.addEventListener('contextmenu', (e) => {
                e.preventDefault();
                e.stopPropagation();
                endCropMode();
                requestShowTooltip("❌ 已取消框选", target);
                return false;
            });

            cropOverlay.addEventListener('mousedown', (e) => {
                if (e.button === 2) return;

                // 2. 限制起点坐标:即使鼠标在图片外按下 起点也会落在图片边缘
                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) => {
                    // 3. 限制终点坐标:鼠标移出图片时 框选框会停留在边缘
                    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; // 稍微放宽最小尺寸限制
                    processCropScan(target, rect);
                };

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

        cropOverlay.style.display = 'block';
        requestShowTooltip("⌛ 框选二维码区域", 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 };
        scanElement(target, true, cropRect);
    }

    // === 统一入口 ===
    function scanElement(target, force = false, cropRect = null) {
        if (target.tagName === 'IMG') {
            scanImage(target, force, cropRect);
        } else if (target.tagName === 'CANVAS') {
            scanCanvas(target, force, cropRect);
        }
    }

    // === 远程解析 ===
    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, { text: resultText, method: "远程解析" });
                        applyQrSuccess(target, resultText, "远程解析");
                    } else {
                        requestShowTooltip("❌ 远程解析失败", target);
                    }
                } else {
                    requestShowTooltip("❌ 远程服务器响应错误: " + response.status, target);
                }
            },
            onerror: function() {
                requestShowTooltip("❌ 网络请求失败", target);
            }
        });
    }

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

    function scanImage(img, force, cropRect) {
        const src = img.src;
        if (!src) return;
        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);
        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) {
        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);
                } 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) {
        // 1. 获取原始尺寸
        let naturalW = imageObj.naturalWidth;
        let naturalH = imageObj.naturalHeight;

        // === 关键修复:增强 SVG 检测 ===
        // 只要 URL 包含 .svg 或 data:image/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;

        // 2. 如果是 SVG/无尺寸 强制使用显示尺寸作为计算基准
        if (forceSimpleMode) {
            naturalW = displayWidth || 300;
            naturalH = displayHeight || 300;
        }

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

        // 如果有框选 且 不是强制简单模式(SVG) 才计算裁剪
        if (cropRect && !forceSimpleMode) {
            const scaleX = naturalW / displayWidth;
            const scaleY = naturalH / displayHeight;
            targetW = cropRect.w * scaleX;
            targetH = cropRect.h * scaleY;
        }

        // 4. 限制最大尺寸 (CROP_TARGET_SIZE)
        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);

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

        // === 核心分支 ===

        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;

            smartDownscale(imageObj, context, sourceX, sourceY, sourceW, sourceH, padding, padding, targetW, targetH);

        } else {
            // 【模式 B:全图模式】
            // 适用于:SVG (无论是否有尺寸)、自动扫描、无框选的位图

            if (forceSimpleMode) {
                // === SVG 专用通道 ===
                // 必须使用 5 参数 drawImage 让浏览器处理矢量缩放
                context.imageSmoothingEnabled = true;
                context.imageSmoothingQuality = 'high';
                context.drawImage(imageObj, padding, padding, targetW, targetH);
            } else {
                // === 普通位图全图扫描 ===
                // 虽然是全图 但为了抗锯齿 依然使用 smartDownscale
                // 此时 source 就是整张图
                smartDownscale(imageObj, context, 0, 0, naturalW, naturalH, padding, padding, targetW, targetH);
            }
        }

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

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

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

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

        // --- 阶段 1: 标准解析 ---

        // 1.1 JSQR 标准
        result = jsQR(imageData.data, imageData.width, imageData.height);
        if (result) {
            handleSuccess(result.data, "JSQR" + suffix, type, cacheKey, targetEl);
            return;
        }

        // 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);
            return;
        }

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

        // 反色数据准备
        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;
        }

        // 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;
        }

        // 二值化数据准备
        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;
        }

        // 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);
    }

    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 = { 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) {
        if (!isForce) {
            if (type === 'IMG') qrCache.set(cacheKey, null);
            else canvasCache.set(targetEl, null);
        }

        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) { return /^https?:\/\//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;

        // 检查缓存
        if (isImg && target.src && qrCache.has(target.src)) {
            const cacheData = qrCache.get(target.src);
            if (cacheData) {
                if (!target.dataset.hasQr) applyQrSuccess(target, cacheData.text, cacheData.method);
                else requestShowTooltip(cacheData.text, target, cacheData.method);
            }
            return;
        }
        if (isCanvas && canvasCache.has(target)) {
            const cacheData = canvasCache.get(target);
            if (cacheData) {
                if (!target.dataset.hasQr) applyQrSuccess(target, cacheData.text, cacheData.method);
                else requestShowTooltip(cacheData.text, target, cacheData.method);
            }
            return;
        }

        let w, h;
        if (isImg) {
            w = target.naturalWidth;
            h = target.naturalHeight;
        } else {
            w = target.width || target.clientWidth;
            h = target.height || target.clientHeight;
        }

        if (Math.abs(w - h) > TOLERANCE || w < 30) {
            if (isImg && target.src) qrCache.set(target.src, null);
            else if (isCanvas) canvasCache.set(target, null);
            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);
                }
                else if (leftClickCount === 3) {
                    startCropMode(interactionTarget);
                }
            }

            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;
            if (target.tagName === 'IMG') {
                const c = qrCache.get(target.src);
                if (c) data = c.text;
            } else {
                const c = canvasCache.get(target);
                if (c) data = c.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);
        }
    });

})();