二维码自动解析

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

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

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

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

您需要先安装一个扩展,例如 篡改猴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.6
// @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 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) {
        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 检测
        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);
    }

    // ==========================================
    //      核心扫描管道 (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) {
        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;

        // 检查缓存
        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);
                }
                // 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;
            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);
        }
    });

})();