Greasy Fork

来自缓存

Greasy Fork is available in English.

网页二维码识别器 - (支持 img/canvas/svg + 精准识别)

修复 SecurityError 和 ReferenceError,支持更多元素类型,精准识别 SVG

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         网页二维码识别器 - (支持 img/canvas/svg + 精准识别)
// @namespace    http://tampermonkey.net/
// @version      8.0
// @description  修复 SecurityError 和 ReferenceError,支持更多元素类型,精准识别 SVG
// @author       hucix
// @match        *://*/*
// @grant        none
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.js
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const HOTKEY = 'q';
    let isSelecting = false;
    let startX, startY;
    let selectionDiv = null;
    let overlay = null;
    let escCloseHandler = null;


    // ✅ 声明并初始化 currentResultUI
    let currentResultUI = null;

    function createOverlay() {
        if (overlay) {
            overlay.remove();
            selectionDiv?.remove();
        }
        overlay = document.createElement('div');
        overlay.style.position = 'fixed';
        overlay.style.top = '0';
        overlay.style.left = '0';
        overlay.style.width = '100%';
        overlay.style.height = '100%';
        overlay.style.background = 'rgba(0,0,0,0.1)';
        overlay.style.zIndex = '2147483646';
        overlay.style.cursor = 'crosshair';
        overlay.style.display = 'none';
        document.body.appendChild(overlay);

        selectionDiv = document.createElement('div');
        selectionDiv.style.position = 'absolute';
        selectionDiv.style.border = '2px dashed #007bff';
        selectionDiv.style.background = 'rgba(0,123,255,0.1)';
        selectionDiv.style.zIndex = '2147483647';
        selectionDiv.style.pointerEvents = 'none';
        document.body.appendChild(selectionDiv);
    }

    function resetState() {
        isSelecting = false;
        if (overlay) {
            overlay.style.display = 'none';
            document.body.style.userSelect = '';
        }
        document.removeEventListener('keydown', escHandler);
        overlay?.removeEventListener('mousedown', startSelect);
        overlay?.removeEventListener('mousemove', updateSelect);
        overlay?.removeEventListener('mouseup', endSelect);
        createOverlay();
    }

    function escHandler(e) {
        if (e.key === 'Escape') resetState();
    }

    function startSelect(e) {
        if (!isSelecting) return;
        startX = e.pageX;
        startY = e.pageY;
        selectionDiv.style.display = 'block';
        selectionDiv.style.left = startX + 'px';
        selectionDiv.style.top = startY + 'px';
        selectionDiv.style.width = '0';
        selectionDiv.style.height = '0';
    }

    function updateSelect(e) {
        if (!isSelecting || selectionDiv.style.display !== 'block') return;
        const x = Math.min(startX, e.pageX);
        const y = Math.min(startY, e.pageY);
        const w = Math.abs(e.pageX - startX);
        const h = Math.abs(e.pageY - startY);
        selectionDiv.style.left = x + 'px';
        selectionDiv.style.top = y + 'px';
        selectionDiv.style.width = w + 'px';
        selectionDiv.style.height = h + 'px';
    }

    // 检查两个矩形是否相交
    function rectsIntersect(r1, r2) {
        return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
    }

    async function recognizeQR(x, y, width, height) {
        const selectionRect = { left: x, top: y, right: x + width, bottom: y + height };
        let found = false;

        // 1. 尝试识别 <img> 元素
        const images = Array.from(document.querySelectorAll('img'));
        for (const img of images) {
            if (!img.complete || img.naturalWidth === 0) continue;
            const imgRect = img.getBoundingClientRect();
            const imgPageRect = {
                left: imgRect.left + window.scrollX,
                top: imgRect.top + window.scrollY,
                right: imgRect.right + window.scrollX,
                bottom: imgRect.bottom + window.scrollY
            };
            if (rectsIntersect(selectionRect, imgPageRect)) {
                try {
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    ctx.drawImage(img, 0, 0);
                    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                    const code = jsQR(imageData.data, canvas.width, canvas.height);
                    if (code) {
                        showResult(code.data);
                        found = true;
                        break;
                    }
                } catch (e) {
                    console.warn('跳过受 CORS 保护的图片:', img.src);
                }
            }
        }

        // 2. 如果没有找到,尝试识别 <canvas> 元素
        if (!found) {
            const canvases = Array.from(document.querySelectorAll('canvas'));
            for (const canvas of canvases) {
                const canvasRect = canvas.getBoundingClientRect();
                const canvasPageRect = {
                    left: canvasRect.left + window.scrollX,
                    top: canvasRect.top + window.scrollY,
                    right: canvasRect.right + window.scrollX,
                    bottom: canvasRect.bottom + window.scrollY
                };
                if (rectsIntersect(selectionRect, canvasPageRect)) {
                    try {
                        const imageData = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
                        const code = jsQR(imageData.data, canvas.width, canvas.height);
                        if (code) {
                            showResult(code.data);
                            found = true;
                            break;
                        }
                    } catch (e) {
                        console.warn('跳过无法读取的 canvas:', canvas);
                    }
                }
            }
        }

        // 3. 如果还没有找到,尝试识别 <svg> 元素(只识别内部的图形)
        if (!found) {
            const svgs = Array.from(document.querySelectorAll('svg'));
            for (const svg of svgs) {
                const svgRect = svg.getBoundingClientRect();
                const svgPageRect = {
                    left: svgRect.left + window.scrollX,
                    top: svgRect.top + window.scrollY,
                    right: svgRect.right + window.scrollX,
                    bottom: svgRect.bottom + window.scrollY
                };
                if (rectsIntersect(selectionRect, svgPageRect)) {
                    try {
                        // 只提取 SVG 中的图形元素(如 path, rect, circle)
                        const graphics = Array.from(svg.querySelectorAll('path, rect, circle, polygon, polyline'));
                        if (graphics.length === 0) {
                            throw new Error('SVG 中没有图形元素');
                        }

                        // 创建一个临时 SVG,只包含图形元素
                        const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                        tempSvg.setAttribute('width', svg.getAttribute('width') || '100%');
                        tempSvg.setAttribute('height', svg.getAttribute('height') || '100%');
                        graphics.forEach(g => {
                            const clone = g.cloneNode(true);
                            tempSvg.appendChild(clone);
                        });

                        // 将临时 SVG 转换为 Data URL
                        const serializer = new XMLSerializer();
                        const source = serializer.serializeToString(tempSvg);
                        const image = new Image();
                        image.onload = () => {
                            const canvas = document.createElement('canvas');
                            canvas.width = image.width;
                            canvas.height = image.height;
                            const ctx = canvas.getContext('2d');
                            ctx.drawImage(image, 0, 0);
                            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
                            const code = jsQR(imageData.data, canvas.width, canvas.height);
                            if (code) {
                                showResult(code.data);
                                found = true;
                            } else {
                                showResult(null, '未识别到二维码。SVG 转换成功,但内容不包含二维码。建议:\n1. 确保框选的是二维码本身\n2. 截图后使用在线工具识别');
                            }
                        };
                        image.onerror = () => {
                            showResult(null, 'SVG 转换失败,无法识别。');
                        };
                        image.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(source)));
                    } catch (e) {
                        console.warn('跳过无法转换的 SVG:', svg);
                    }
                }
            }
        }

        // 4. 如果以上都失败,显示友好提示
        if (!found) {
            showResult(null, '未在选区中找到可识别的二维码。\n\n建议:\n1. 确保框选的是图片或画布元素\n2. 截图后使用在线工具识别');
        }
    }

    function showResult(text, errorMsg = null) {
    // 移除旧的 UI(如果存在)
    if (currentResultUI) {
        currentResultUI.remove();
    }

    currentResultUI = document.createElement('div');
    currentResultUI.id = 'qr-result-ui';
    currentResultUI.style.position = 'fixed';
    currentResultUI.style.top = '50%';
    currentResultUI.style.left = '50%';
    currentResultUI.style.transform = 'translate(-50%, -50%)';
    currentResultUI.style.zIndex = '2147483647';
    currentResultUI.style.background = '#fff';
    currentResultUI.style.padding = '20px';
    currentResultUI.style.borderRadius = '8px';
    currentResultUI.style.boxShadow = '0 4px 20px rgba(0,0,0,0.3)';
    currentResultUI.style.maxWidth = '90%';
    currentResultUI.style.wordBreak = 'break-all';
    currentResultUI.style.fontFamily = 'Arial, sans-serif';

    let content = '';

    if (errorMsg) {
        content = `<h3 style="color:#dc3545;">识别失败</h3><p>${errorMsg}</p>`;
    } else {
        let isUrl = false;
        try { new URL(text); isUrl = true; } catch {}

        content = `
            <h3>识别到内容:</h3>
            <pre style="background:#f8f9fa; padding:10px; border-radius:4px; overflow:auto; max-height:200px; font-family:monospace; white-space:pre-wrap;">${text}</pre>
        `;
        if (isUrl) {
            content += `<button id="qr-jump-btn" style="margin-top:10px; padding:6px 12px; background:#007bff; color:white; border:none; border-radius:4px;">跳转到链接</button>`;
        }
        content += `<button id="qr-copy-btn" style="margin-left:10px; padding:6px 12px; background:#28a745; color:white; border:none; border-radius:4px;">复制内容</button>`;
    }

    content += `<button id="qr-close-btn" style="margin-top:10px; padding:6px 12px; background:#6c757d; color:white; border:none; border-radius:4px; margin-left:10px;">关闭</button>`;

    currentResultUI.innerHTML = content;
    document.body.appendChild(currentResultUI);

    // 定义 ESC 关闭函数
    escCloseHandler = (e) => {
        if (e.key === 'Escape') {
            closeResultUI();
        }
    };

    // 绑定 ESC 监听
    document.addEventListener('keydown', escCloseHandler);

    // 安全绑定按钮事件
    const jumpBtn = document.getElementById('qr-jump-btn');
    const copyBtn = document.getElementById('qr-copy-btn');
    const closeBtn = document.getElementById('qr-close-btn');

    if (jumpBtn && text) {
        jumpBtn.onclick = () => { window.open(text, '_blank') };
    }
    if (copyBtn && text) {
        copyBtn.onclick = () => {
            navigator.clipboard.writeText(text).then(() => {
                alert('已复制到剪贴板!');
            }).catch(() => {
                prompt('复制失败,请手动复制:', text);
            });
        };
    }
    if (closeBtn) {
        closeBtn.onclick = closeResultUI;
    }

    resetState(); // 清理框选状态
}

// 关闭识别结果 UI 的统一函数
function closeResultUI() {
    if (currentResultUI) {
        currentResultUI.remove();
        currentResultUI = null;
    }
    // 移除 ESC 监听器
    if (escCloseHandler) {
        document.removeEventListener('keydown', escCloseHandler);
        escCloseHandler = null;
    }
}

    function endSelect() {
        if (!isSelecting) return;
        isSelecting = false;
        overlay.style.display = 'none';
        document.body.style.userSelect = '';
        document.removeEventListener('keydown', escHandler);

        const rect = selectionDiv.getBoundingClientRect();
        const x = rect.left + window.scrollX;
        const y = rect.top + window.scrollY;
        const w = rect.width;
        const h = rect.height;

        selectionDiv.style.display = 'none';

        if (w > 20 && h > 20) {
            recognizeQR(x, y, w, h);
        } else {
            showResult(null, '选区太小,请框选更大的二维码区域(至少 20x20 像素)。');
            resetState();
        }
    }

    function init() {
        createOverlay();
        document.addEventListener('keydown', (e) => {
            if (e.ctrlKey && e.key.toLowerCase() === HOTKEY && !isSelecting) {
                e.preventDefault();
                isSelecting = true;
                overlay.style.display = 'block';
                document.body.style.userSelect = 'none';
                document.addEventListener('keydown', escHandler);
                overlay.addEventListener('mousedown', startSelect);
                overlay.addEventListener('mousemove', updateSelect);
                overlay.addEventListener('mouseup', endSelect);
            }
        });
    }

    if (typeof jsQR !== 'undefined') {
        init();
    } else {
        const script = document.createElement('script');
        script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/jsQR.js';
        script.onload = init;
        document.head.appendChild(script);
    }
})();