网页图片采集器

下载网页中的图片

当前为 2025-10-18 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         网页图片采集器
// @namespace    http://tampermonkey.net/
// @version      4.1
// @description  下载网页中的图片
// @author       YourName
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_notification
// @grant        GM_download
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @icon         https://cdn-icons-png.flaticon.com/512/2107/2107957.png
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';
    const CONFIG = {
        buttonSize: 30,
        activeColor: '#e74c3c',
        hoverColor: '#c0392b',
        zIndex: 99999,
        positionOffset: 25,
        touchDelay: 300,
        supportFormats: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'tiff'],
        maxPreviewSize: 50,
        loadTimeout: 5000,
        infoTruncateLength: 4,
        panelSafeMargin: '20px',
        panelMinSize: '320px',
        // 后缀补全配置:无后缀时默认添加的格式
        defaultImageFormat: 'png'
    };

    GM_addStyle(`
        /*按钮样式 */
        .radar-container {position:fixed;z-index:${CONFIG.zIndex};cursor:move;transition:transform 0.2s;touch-action:none;}
        .radar-button {width:${CONFIG.buttonSize}px;height:${CONFIG.buttonSize}px;border-radius:50%;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#e74c3c,#922b21);box-shadow:0 6px 18px rgba(0,0,0,0.3),0 0 0 4px rgba(255,255,255,0.15),inset 0 0 12px rgba(0,0,0,0.3);cursor:pointer;border:none;outline:none;position:relative;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;animation:pulse 2s infinite;transition:transform 0.3s,box-shadow 0.3s;}
        .radar-button:hover {transform:scale(1.05);box-shadow:0 8px 22px rgba(0,0,0,0.4),0 0 0 4px rgba(255,255,255,0.25),inset 0 0 15px rgba(0,0,0,0.4);}
        .radar-button:active {transform:scale(0.95);}
        .radar-icon {width:24px;height:24px;position:relative;display:flex;justify-content:center;align-items:center;filter:drop-shadow(0 0 2px rgba(255,255,255,0.5));animation:radar-scan 4s linear infinite;}
        .radar-icon svg {width:100%;height:100%;}

        /* 面板核心样式:自适应视口 */
        #svgSnifferModal {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            margin: ${CONFIG.panelSafeMargin};
            max-width: calc(100vw - 2 * ${CONFIG.panelSafeMargin});
            max-height: calc(100vh - 2 * ${CONFIG.panelSafeMargin});
            min-width: ${CONFIG.panelMinSize};
            min-height: ${CONFIG.panelMinSize};
            width: auto;
            height: auto;
            z-index: 10000;
            background: #fff;
            border-radius: 12px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
            border: 1px solid #ddd;
            display: none;
            flex-direction: column;
            font-family: Arial, sans-serif;
            overflow: hidden;
        }

        /* 面板头部 */
        .modal-header {
            padding: 12px 20px;
            background:linear-gradient(135deg,#e74c3c,#922b21);
            color: #fff;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
        }
        .modal-header h2 {margin: 0;font-size: 1.2rem;font-weight: 600;}
        .close-btn {
            background: none;
            border: none;
            color: #fff;
            font-size: 1.5rem;
            cursor: pointer;
            width: 32px;
            height: 32px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }
        .close-btn:hover {background: rgba(255,255,255,0.2);}

        /* 操作栏 */
        .action-bar {
            padding: 10px 20px;
            background: #f8f9fa;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-shrink: 0;
            flex-wrap: wrap;
            gap: 10px;
        }
        .select-all-control {
            display: flex;
            align-items: center;
            gap: 8px;
            font-size: 0.9rem;
        }
        .action-buttons {
            display: flex;
            gap: 8px;
            flex-wrap: wrap;
        }
        .action-btn {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.9rem;
            transition: all 0.2s;
            white-space: nowrap;
        }
        .download-btn {background: #27ae60;color: #fff;}
        .download-btn:hover {background: #219653;transform: translateY(-2px);}
        .copy-btn {background: #2980b9;color: #fff;}
        .copy-btn:hover {background: #2573a7;transform: translateY(-2px);}

        /* 内容区 */
        .modal-content {
            padding: 15px;
            overflow-y: auto;
            flex-grow: 1;
            min-height: 0;
        }

        /* 图片项 */
        .svg-item {
            display: flex;
            align-items: center;
            padding: 12px;
            border-bottom: 1px solid #eee;
            transition: background 0.2s;
            justify-content: space-between;
            gap: 10px;
            min-width: 0;
        }
        .svg-item:hover {background: #f8fafc;}
        .svg-checkbox {
            width: 18px;
            height: 18px;
            cursor: pointer;
            flex-shrink: 0;
        }
        .svg-preview {
            width: ${CONFIG.maxPreviewSize}px;
            height: ${CONFIG.maxPreviewSize}px;
            border: 1px solid #e0e0e0;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 6px;
            background-image: 
                linear-gradient(45deg, #e0e0e0 25%, transparent 25%),
                linear-gradient(-45deg, #e0e0e0 25%, transparent 25%),
                linear-gradient(45deg, transparent 75%, #e0e0e0 75%),
                linear-gradient(-45deg, transparent 75%, #e0e0e0 75%);
            background-size: 10px 10px;
            background-position: 0 0, 0 5px, 5px -5px, -5px 0px;
            background-color: #f8f8f8;
            box-shadow: 0 2px 6px rgba(0,0,0,0.1);
            overflow: hidden;
            flex-shrink: 0;
        }
        .svg-preview img, .svg-preview svg {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
            display: block;
        }

        /* 图片信息 */
        .svg-info {
            flex-grow: 1;
            min-width: 0;
        }
        .svg-name {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-size: 1rem;
            color: #2c3e50;
            margin-bottom: 3px;
            max-width: 80px;
        }
        .svg-meta {
            font-size: 0.8rem;
            color: #6e6e73;
            display: flex;
            gap: 12px;
            flex-wrap: wrap;
        }
        .svg-meta span {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 80px;
        }

        /* 单独下载按钮 */
        .item-download-btn {
            padding: 5px 10px;
            background: #e74c3c;
            color: #fff;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.8rem;
            transition: background 0.2s;
            white-space: nowrap;
            flex-shrink: 0;
        }
        .item-download-btn:hover {background: #c0392b;}

        /* 其他基础样式 */
        .overlay {display: none;position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0,0,0,0.5);z-index: 9999;}
        .loading {text-align: center;padding: 25px;font-size: 1.1rem;color: #666;}
        .copy-notification {
            position: fixed;
            top: 30px;
            left: 50%;
            transform: translateX(-50%);
            background: #27ae60;
            color: #fff;
            padding: 10px 20px;
            border-radius: 6px;
            z-index: 100000;
            opacity: 0;
            transition: opacity 0.5s;
            pointer-events: none;
            white-space: nowrap;
            font-weight: 500;
        }

        /* 动画样式 */
        @keyframes radar-scan {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
        @keyframes pulse {0% {box-shadow: 0 0 0 0 rgba(231,76,60,0.6);} 70% {box-shadow: 0 0 0 12px rgba(231,76,60,0);} 100% {box-shadow: 0 0 0 0 rgba(231,76,60,0);}}
        .temp-visible-for-scan {display: block !important;visibility: visible !important;opacity: 1 !important;position: absolute !important;top: -9999px !important;left: -9999px !important;width: auto !important;height: auto !important;}
    `);

    const radarContainer = document.createElement('div');
    radarContainer.className = 'radar-container';
    radarContainer.id = 'radarContainer';
    
    const radarButton = document.createElement('div');
    radarButton.className = 'radar-button';
    radarButton.id = 'radarButton';
    radarButton.innerHTML = `
        <div class="radar-icon">
            <svg viewBox="0 0 24 24" fill="white" xmlns="http://www.w3.org/2000/svg">
                <path d="M19 3H5C3.89 3 3 3.9 3 5V19C3 20.1 3.89 21 5 21H19C20.11 21 21 20.1 21 19V5C21 3.9 20.11 3 19 3ZM19 19H5V5H19V19ZM12 12C10.34 12 9 10.66 9 9C9 7.34 10.34 6 12 6C13.66 6 15 7.34 15 9C15 10.66 13.66 12 12 12ZM12 8C11.45 8 11 8.45 11 9C11 9.55 11.45 10 12 10C12.55 10 13 9.55 13 9C13 8.45 12.55 8 12 8Z"/>
                <path d="M16.59 16.59L13.7 18.65C13.25 18.96 12.63 19 12 19C11.37 19 10.75 18.96 10.3 18.65L7.41 16.59L8.58 14.41L10.54 15.91C10.91 16.15 11.45 16.15 11.82 15.91L16.59 12.59L17.76 14.41L16.59 16.59Z"/>
            </svg>
        </div>
    `;
    
    radarContainer.appendChild(radarButton);
    document.body.appendChild(radarContainer);

    const svgModal = document.createElement('div');
    svgModal.id = 'svgSnifferModal';
    svgModal.innerHTML = `
        <div class="modal-header">
            <h2>图片列表</h2>
            <button class="close-btn">&times;</button>
        </div>
        <div class="action-bar">
            <div class="select-all-control">
                <input type="checkbox" id="selectAll">
                <label for="selectAll">全选(共<span id="imageCount">0</span>张)</label>
            </div>
            <div class="action-buttons">
                <button class="action-btn download-btn" id="batchDownloadBtn">批量下载</button>
                <button class="action-btn copy-btn">复制链接</button>
            </div>
        </div>
        <div class="modal-content" id="svgList">
            <div class="loading">正在扫描页面图片资源(含CSS/隐藏元素)...</div>
        </div>
    `;
    document.body.appendChild(svgModal);

    const overlay = document.createElement('div');
    overlay.className = 'overlay';
    document.body.appendChild(overlay);

    const copyNotification = document.createElement('div');
    copyNotification.className = 'copy-notification';
    document.body.appendChild(copyNotification);

    let globalImageItems = [];
    let imageItemCache = new Map();
    let isDragging = false;
    let startX, startY, startLeft, startTop;
    let dragStartTime = 0;
    let touchTimer = null;
    let blobUrls = [];
    let tempVisibleElements = [];

    // 4字节截断工具函数
    function truncateTo4Bytes(text) {
        if (!text || typeof text !== 'string') return '';
        
        let byteCount = 0;
        let result = '';
        
        for (let i = 0; i < text.length; i++) {
            const char = text[i];
            const charBytes = /[\u4e00-\u9fa5]/.test(char) ? 2 : 1;
            
            if (byteCount + charBytes > CONFIG.infoTruncateLength) {
                break;
            }
            
            result += char;
            byteCount += charBytes;
            
            if (byteCount === CONFIG.infoTruncateLength) {
                break;
            }
        }
        
        return result;
    }

    // ========== 新增:文件后缀处理工具函数 ==========
    /**
     * 处理文件名,无图片后缀时自动添加默认格式(.png)
     * @param {string} fileName - 原始文件名
     * @param {string} originalFormat - 图片原始格式(可能为空)
     * @returns {string} 补全后缀后的完整文件名
     */
    function completeImageSuffix(fileName, originalFormat) {
        // 1. 提取现有后缀
        const extMatch = fileName.match(/\.([^.]+)$/);
        const hasValidSuffix = extMatch 
            ? CONFIG.supportFormats.includes(extMatch[1].toLowerCase()) 
            : false;
        
        // 2. 已有有效后缀,直接返回
        if (hasValidSuffix) return fileName;
        
        // 3. 无有效后缀,使用原始格式或默认格式(.png)
        const targetFormat = (originalFormat && CONFIG.supportFormats.includes(originalFormat.toLowerCase())) 
            ? originalFormat.toLowerCase() 
            : CONFIG.defaultImageFormat;
        
        // 4. 避免重复添加后缀(如文件名已含".但无后缀")
        return fileName.endsWith('.') 
            ? `${fileName}${targetFormat}` 
            : `${fileName}.${targetFormat}`;
    }
    // ========== 工具函数结束 ==========

    // 处理SVG内容使其在预览框中正确显示
    function processSVGForPreview(svgContent) {
        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(svgContent, 'image/svg+xml');
            const svgElement = doc.documentElement;
            
            const width = svgElement.getAttribute('width') || svgElement.getAttribute('viewBox')?.split(' ')[2] || CONFIG.maxPreviewSize;
            const height = svgElement.getAttribute('height') || svgElement.getAttribute('viewBox')?.split(' ')[3] || CONFIG.maxPreviewSize;
            
            svgElement.setAttribute('width', '100%');
            svgElement.setAttribute('height', '100%');
            svgElement.setAttribute('preserveAspectRatio', 'xMidYMid meet');
            
            if (!svgElement.hasAttribute('viewBox')) {
                svgElement.setAttribute('viewBox', `0 0 ${width} ${height}`);
            }
            
            return svgElement.outerHTML;
        } catch (error) {
            console.warn('SVG处理失败,使用原始内容:', error);
            return svgContent;
        }
    }

    // 基础图片嗅探方式
    function collectBasicImages() {
        const images = [];
        const imgElements = document.querySelectorAll('img');
        imgElements.forEach((img, index) => {
            const src = img.src || img.dataset.src || img.currentSrc;
            if (src && !src.startsWith('data:')) {
                const truncatedName = truncateTo4Bytes(img.alt || '未命名图片');
                const originalFormat = getFileExtension(src).toLowerCase();
                const format = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat);
                
                images.push({
                    id: `basic-img-${index}`,
                    url: src,
                    name: truncatedName,
                    format: format,
                    width: img.naturalWidth || img.width || '未知',
                    height: img.naturalHeight || img.height || '未知',
                    type: 'img-tag',
                    preview: src,
                    element: img,
                    originalName: img.alt || '未命名图片',
                    originalFormat: originalFormat,
                    svgContent: ''
                });
            }
        });
        return images;
    }

    function initRadarButton() {
        const domain = location.hostname.replace(/\./g, '-');
        const positionKey = `radarPosition_${domain}`;
        
        const savedPosition = GM_getValue(positionKey);
        if (savedPosition) {
            radarContainer.style.left = `${savedPosition.x}px`;
            radarContainer.style.top = `${savedPosition.y}px`;
        } else {
            radarContainer.style.right = `${CONFIG.positionOffset}px`;
            radarContainer.style.bottom = `${CONFIG.positionOffset}px`;
        }
        
        radarContainer.addEventListener('mousedown', startDrag);
        radarContainer.addEventListener('touchstart', startDrag, { passive: false });
        
        radarButton.addEventListener('click', (e) => {
            if (!isDragging && Date.now() - dragStartTime > CONFIG.touchDelay) {
                showImageList();
            }
        });
    }

    function startDrag(e) {
        e.preventDefault();
        const clientX = e.clientX || e.touches[0].clientX;
        const clientY = e.clientY || e.touches[0].clientY;
        
        const computedStyle = window.getComputedStyle(radarContainer);
        startLeft = parseInt(computedStyle.left) || 0;
        startTop = parseInt(computedStyle.top) || 0;
        
        if (computedStyle.right !== 'auto') {
            const rightPos = parseInt(computedStyle.right);
            startLeft = window.innerWidth - rightPos - CONFIG.buttonSize;
            radarContainer.style.right = 'auto';
            radarContainer.style.left = `${startLeft}px`;
        }
        
        startX = clientX;
        startY = clientY;
        dragStartTime = Date.now();
        
        if (e.type === 'touchstart') {
            touchTimer = setTimeout(() => {
                isDragging = true;
                radarContainer.style.transition = 'none';
            }, CONFIG.touchDelay);
        } else {
            isDragging = true;
        }
        
        document.addEventListener('mousemove', drag);
        document.addEventListener('touchmove', drag, { passive: false });
        document.addEventListener('mouseup', endDrag);
        document.addEventListener('touchend', endDrag);
    }

    function drag(e) {
        if (!isDragging) return;
        e.preventDefault();
        const clientX = e.clientX || e.touches[0].clientX;
        const clientY = e.clientY || e.touches[0].clientY;
        
        const dx = clientX - startX;
        const dy = clientY - startY;
        radarContainer.style.left = `${startLeft + dx}px`;
        radarContainer.style.top = `${startTop + dy}px`;
        radarContainer.style.right = 'auto';
    }

    function endDrag(e) {
        if (touchTimer) {
            clearTimeout(touchTimer);
            touchTimer = null;
        }
        
        if (!isDragging) {
            if (Date.now() - dragStartTime < CONFIG.touchDelay) {
                showImageList();
            }
            return;
        }
        
        isDragging = false;
        radarContainer.style.transition = '';
        
        document.removeEventListener('mousemove', drag);
        document.removeEventListener('touchmove', drag);
        document.removeEventListener('mouseup', endDrag);
        document.removeEventListener('touchend', endDrag);
        
        const domain = location.hostname.replace(/\./g, '-');
        const positionKey = `radarPosition_${domain}`;
        const rect = radarContainer.getBoundingClientRect();
        GM_setValue(positionKey, {
            x: rect.left,
            y: rect.top
        });
    }

    function tempShowHiddenElements() {
        tempVisibleElements = [];
        const hiddenSelectors = [
            'div[style*="display:none"]',
            'div[style*="visibility:hidden"]',
            'div[style*="opacity:0"]',
            '.errorpage[style*="display:none"]',
            '[class*="hidden"]',
            '[hidden]'
        ];
        hiddenSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(el => {
                const originalStyle = {
                    display: el.style.display,
                    visibility: el.style.visibility,
                    opacity: el.style.opacity,
                    position: el.style.position,
                    top: el.style.top,
                    left: el.style.left,
                    width: el.style.width,
                    height: el.style.height,
                    className: el.className
                };
                tempVisibleElements.push({ el, originalStyle });
                el.classList.add('temp-visible-for-scan');
                el.style.display = '';
                el.style.visibility = '';
                el.style.opacity = '';
            });
        });
    }

    function restoreHiddenElements() {
        tempVisibleElements.forEach(({ el, originalStyle }) => {
            el.classList.remove('temp-visible-for-scan');
            el.style.display = originalStyle.display;
            el.style.visibility = originalStyle.visibility;
            el.style.opacity = originalStyle.opacity;
            el.style.position = originalStyle.position;
            el.style.top = originalStyle.top;
            el.style.left = originalStyle.left;
            el.style.width = originalStyle.width;
            el.style.height = originalStyle.height;
            el.className = originalStyle.className;
        });
        tempVisibleElements = [];
    }

    async function collectImagesFromCss(cssUrl) {
        const imageUrls = [];
        try {
            const response = await fetch(cssUrl, {
                headers: {
                    'Accept': 'text/css,*/*;q=0.1'
                },
                credentials: 'same-origin'
            });
            if (!response.ok) throw new Error(`CSS请求失败: ${response.status}`);
            
            const cssText = await response.text();
            const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi;
            let match;
            while ((match = bgUrlRegex.exec(cssText)) !== null) {
                if (match[1]) {
                    const fullUrl = new URL(match[1], cssUrl).href;
                    const ext = getFileExtension(fullUrl).toLowerCase();
                    if (CONFIG.supportFormats.includes(ext)) {
                        imageUrls.push(fullUrl);
                    }
                }
            }
        } catch (e) {
            console.warn('采集CSS中的图片失败:', cssUrl, e);
        }
        return imageUrls;
    }

    async function collectAllCssResources() {
        const cssUrls = [];
        const linkElements = document.querySelectorAll('link[rel="stylesheet"]');
        linkElements.forEach(link => {
            const href = link.getAttribute('href');
            if (href) {
                cssUrls.push(new URL(href, window.location.href).href);
            }
        });
        const styleElements = document.querySelectorAll('style');
        styleElements.forEach(style => {
            const bgUrlRegex = /background-image\s*:\s*url\(["']?([^"']+)["']?\)/gi;
            let match;
            while ((match = bgUrlRegex.exec(style.textContent)) !== null) {
                if (match[1]) {
                    const fullUrl = new URL(match[1], window.location.href).href;
                    const ext = getFileExtension(fullUrl).toLowerCase();
                    if (CONFIG.supportFormats.includes(ext)) {
                        cssUrls.push(`inline:${fullUrl}`);
                    }
                }
            }
        });
        const allImageUrls = [];
        for (const cssUrl of cssUrls) {
            if (cssUrl.startsWith('inline:')) {
                allImageUrls.push(cssUrl.replace('inline:', ''));
            } else {
                const imagesFromCss = await collectImagesFromCss(cssUrl);
                allImageUrls.push(...imagesFromCss);
            }
        }
        return allImageUrls;
    }

    async function collectImages() {
        const imageItems = [];
        const processedUrls = new Set();
        let cssImageUrls = [];
        
        const basicImages = collectBasicImages();
        basicImages.forEach(img => {
            if (!processedUrls.has(img.url)) {
                processedUrls.add(img.url);
                imageItems.push(img);
            }
        });
        
        try {
            cssImageUrls = await collectAllCssResources();
        } catch (e) {
            console.warn('CSS图片采集异常:', e);
        }
        
        tempShowHiddenElements();
        try {
            // 增强的img标签采集
            const imgElements = document.querySelectorAll('img');
            for (const img of imgElements) {
                try {
                    let imgUrl = img.src || img.dataset.src || img.dataset.original || img.currentSrc;
                    if (!imgUrl || processedUrls.has(imgUrl) || imgUrl.startsWith('data:')) continue;
                    const fullUrl = new URL(imgUrl, window.location.href).href;
                    const originalFormat = getFileExtension(fullUrl).toLowerCase();
                    if (!CONFIG.supportFormats.includes(originalFormat) && originalFormat) continue;
                    if (processedUrls.has(fullUrl)) continue;
                    
                    const originalName = getImageName(fullUrl, img.alt);
                    const truncatedName = truncateTo4Bytes(originalName);
                    const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat);
                    
                    let svgContent = '';
                    if (originalFormat === 'svg') {
                        try {
                            const svgResponse = await fetch(fullUrl);
                            if (svgResponse.ok) {
                                svgContent = await svgResponse.text();
                            }
                        } catch (svgErr) {
                            console.warn('获取SVG原始内容失败:', svgErr);
                        }
                    }
                    
                    const imgInfo = await new Promise((resolve) => {
                        const timer = setTimeout(() => {
                            resolve({
                                id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                                url: fullUrl,
                                name: truncatedName,
                                format: truncatedFormat,
                                width: img.width || '未知',
                                height: img.height || '未知',
                                type: 'img-tag',
                                preview: fullUrl,
                                originalName: originalName,
                                originalFormat: originalFormat,
                                svgContent: svgContent
                            });
                        }, CONFIG.loadTimeout);
                        
                        if (img.complete) {
                            clearTimeout(timer);
                            resolve({
                                id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                                url: fullUrl,
                                name: truncatedName,
                                format: truncatedFormat,
                                width: img.naturalWidth || img.width || '未知',
                                height: img.naturalHeight || img.height || '未知',
                                type: 'img-tag',
                                preview: fullUrl,
                                originalName: originalName,
                                originalFormat: originalFormat,
                                svgContent: svgContent
                            });
                        } else {
                            img.onload = () => {
                                clearTimeout(timer);
                                resolve({
                                    id: `img-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                                    url: fullUrl,
                                    name: truncatedName,
                                    format: truncatedFormat,
                                    width: img.naturalWidth || img.width || '未知',
                                    height: img.naturalHeight || img.height || '未知',
                                    type: 'img-tag',
                                    preview: fullUrl,
                                    originalName: originalName,
                                    originalFormat: originalFormat,
                                    svgContent: svgContent
                                });
                            };
                            img.onerror = () => {
                                clearTimeout(timer);
                                resolve(null);
                            };
                        }
                    });
                    
                    if (imgInfo) {
                        processedUrls.add(imgUrl);
                        imageItems.push(imgInfo);
                    }
                } catch (e) {
                    console.warn('采集<img>标签失败:', e);
                }
            }
            
            // 背景图采集
            const elementsWithBg = document.querySelectorAll('*');
            for (const el of elementsWithBg) {
                try {
                    const bgStyle = window.getComputedStyle(el).backgroundImage;
                    if (!bgStyle || bgStyle === 'none' || processedUrls.has(bgStyle)) continue;
                    const bgUrls = bgStyle.match(/url\(["']?([^"']+)["']?\)/g);
                    if (!bgUrls) continue;
                    
                    for (const bgUrl of bgUrls) {
                        try {
                            const match = bgUrl.match(/url\(["']?([^"']+)["']?\)/);
                            if (!match || !match[1]) continue;
                            let imgUrl = match[1];
                            if (processedUrls.has(imgUrl)) continue;
                            
                            const fullUrl = new URL(imgUrl, window.location.href).href;
                            const originalFormat = getFileExtension(fullUrl).toLowerCase();
                            
                            let svgContent = '';
                            if (originalFormat === 'svg') {
                                try {
                                    const svgResponse = await fetch(fullUrl);
                                    if (svgResponse.ok) {
                                        svgContent = await svgResponse.text();
                                    }
                                } catch (svgErr) {
                                    console.warn('获取背景SVG内容失败:', svgErr);
                                }
                            }
                            
                            const originalName = `背景图-${el.tagName.toLowerCase()}-${Date.now().toString().slice(-4)}`;
                            const truncatedName = truncateTo4Bytes(originalName);
                            const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat);
                            const truncatedType = truncateTo4Bytes('背景图');
                            
                            const imgInfo = {
                                id: `bg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                                url: fullUrl,
                                name: truncatedName,
                                format: truncatedFormat,
                                width: '背景图',
                                height: '背景图',
                                type: truncatedType,
                                preview: fullUrl,
                                originalName: originalName,
                                originalFormat: originalFormat,
                                originalType: '背景图',
                                svgContent: svgContent
                            };
                            processedUrls.add(imgUrl);
                            imageItems.push(imgInfo);
                        } catch (e) {
                            console.warn('采集背景图失败:', e);
                        }
                    }
                } catch (e) {
                    console.warn('处理背景图样式失败:', e);
                }
            }
            
            // CSS图片采集
            for (const imgUrl of cssImageUrls) {
                try {
                    if (!imgUrl || processedUrls.has(imgUrl)) continue;
                    const fullUrl = new URL(imgUrl, window.location.href).href;
                    const originalFormat = getFileExtension(fullUrl).toLowerCase();
                    
                    let svgContent = '';
                    if (originalFormat === 'svg') {
                        try {
                            const svgResponse = await fetch(fullUrl);
                            if (svgResponse.ok) {
                                svgContent = await svgResponse.text();
                            }
                        } catch (svgErr) {
                            console.warn('获取CSS SVG内容失败:', svgErr);
                        }
                    }
                    
                    const originalName = `CSS图片-${Date.now().toString().slice(-4)}`;
                    const truncatedName = truncateTo4Bytes(originalName);
                    const truncatedFormat = truncateTo4Bytes(originalFormat || CONFIG.defaultImageFormat);
                    const truncatedType = truncateTo4Bytes('CSS图片');
                    
                    const imgInfo = {
                        id: `css-${Date.now()}-${Math.random().toString(36).slice(2)}`,
                        url: fullUrl,
                        name: truncatedName,
                        format: truncatedFormat,
                        width: 'CSS引用',
                        height: 'CSS引用',
                        type: truncatedType,
                        preview: fullUrl,
                        originalName: originalName,
                        originalFormat: originalFormat,
                        originalType: 'CSS图片',
                        svgContent: svgContent
                    };
                    processedUrls.add(imgUrl);
                    imageItems.push(imgInfo);
                } catch (e) {
                    console.warn('采集CSS图片失败:', e);
                }
            }
            
            // SVG标签采集
            const svgElements = document.querySelectorAll('svg');
            for (const svg of svgElements) {
                try {
                    const svgId = `svg-${Date.now()}-${Math.random().toString(36).slice(2)}`;
                    const svgContent = svg.outerHTML;
                    const processedSvgContent = processSVGForPreview(svgContent);
                    const svgUrl = URL.createObjectURL(new Blob([svgContent], { type: 'image/svg+xml' }));
                    blobUrls.push(svgUrl);
                    
                    const originalName = `SVG图片-${Date.now().toString().slice(-4)}`;
                    const truncatedName = truncateTo4Bytes(originalName);
                    const truncatedFormat = truncateTo4Bytes('svg');
                    const truncatedType = truncateTo4Bytes('SVG标签');
                    
                    const imgInfo = {
                        id: svgId,
                        url: svgUrl,
                        name: truncatedName,
                        format: truncatedFormat,
                        width: svg.naturalWidth || svg.width.baseVal.value || '自适应',
                        height: svg.naturalHeight || svg.height.baseVal.value || '自适应',
                        type: truncatedType,
                        preview: svgUrl,
                        svgContent: svgContent,
                        originalName: originalName,
                        originalFormat: 'svg',
                        originalType: 'SVG标签'
                    };
                    imageItems.push(imgInfo);
                } catch (e) {
                    console.warn('采集SVG标签失败:', e);
                }
            }
        } catch (e) {
            console.error('图片采集主流程异常:', e);
        } finally {
            restoreHiddenElements();
        }
        return imageItems;
    }

    function getFileExtension(url) {
        const path = new URL(url).pathname;
        const lastPart = path.split('/').pop();
        const extMatch = lastPart.match(/\.([^.]+)$/);
        return extMatch ? extMatch[1].toLowerCase() : '';
    }

    function getImageName(url, altText) {
        if (altText && altText.trim()) {
            return altText.trim();
        }
        const path = new URL(url).pathname;
        const fileName = path.split('/').pop().split('?')[0].split('#')[0];
        return fileName || `未知图片-${Date.now().toString().slice(-6)}`;
    }

    async function showImageList() {
        const modal = document.getElementById('svgSnifferModal');
        const svgList = document.getElementById('svgList');
        const imageCountEl = document.getElementById('imageCount');
        
        svgList.innerHTML = '<div class="loading">正在扫描页面图片资源(含CSS/隐藏元素)...</div>';
        modal.style.display = 'flex';
        overlay.style.display = 'block';
        
        try {
            const imageItems = await collectImages();
            globalImageItems = imageItems;
            imageItemCache.clear();
            imageCountEl.textContent = imageItems.length;
            imageItems.forEach(item => {
                imageItemCache.set(item.id, item);
            });
            
            if (imageItems.length === 0) {
                svgList.innerHTML = '<div class="loading">没有找到任何图片资源(已尝试采集隐藏元素和CSS)</div>';
                return;
            }
            
            svgList.innerHTML = '';
            imageItems.forEach(item => {
                const itemElement = document.createElement('div');
                itemElement.className = 'svg-item';
                
                const truncatedTitle = item.name;
                const fullName = item.originalName || truncatedTitle;
                const fullFormat = item.originalFormat || CONFIG.defaultImageFormat;
                const fullType = item.originalType || item.type;
                // 显示补全后的文件名(用于tooltip提示)
                const completedFileName = completeImageSuffix(fullName, fullFormat);
                
                let previewHtml = '';
                if (item.format === 'svg' && item.svgContent) {
                    previewHtml = item.svgContent;
                } else {
                    previewHtml = `<img src="${item.preview}" alt="${fullName}" loading="lazy" onError="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTAiIGhlaWdodD0iNTAiIHZpZXdCb3g9IjAgMCA1MCA1MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjUwIiBoZWlnaHQ9IjUwIiByeD0iMTUiIGZpbGw9IiNmNGY0ZjQiLz4KPHBhdGggZD0iTTIwIDI1QzIwIDI1IDIyIDIyIDI1IDIyQzI4IDIyIDMwIDI1IDMwIDI1QzMwIDI1IDI4IDI4IDI1IDI4QzIyIDI4IDIwIDI1IDIwIDI1WiIgc3Ryb2tlPSIjNzc3IiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTMwIDI1QzMwIDI1IDI4IDI4IDI1IDI4QzIyIDI4IDIwIDI1IDIwIDI1QzIwIDI1IDIyIDIyIDI1IDIyQzI4IDIyIDMwIDI1IDMwIDI1WiIgc3Ryb2tlPSIjNzc3IiBzdHJva2Utd2lkdGg9IjIiLz4KPHBhdGggZD0iTTI1IDMwQzI1IDMwIDI1IDM1IDI1IDM1QzI1IDM1IDI1IDMwIDI1IDMwWiIgc3Ryb2tlPSIjNzc3IiBzdHJva2Utd2lkdGg9IjIiLz4KPC9zdmc+Cg=='">`;
                }
                
                itemElement.innerHTML = `
                    <div style="display: flex; align-items: center; min-width: 0; gap: 10px;">
                        <input type="checkbox" class="svg-checkbox" data-id="${item.id}" checked>
                        <div class="svg-preview">${previewHtml}</div>
                        <div class="svg-info">
                            <div class="svg-name" title="文件名:${completedFileName}">${truncatedTitle}</div>
                            <div class="svg-meta">
                                <span title="格式:${fullFormat || CONFIG.defaultImageFormat}">格式: ${item.format}</span>
                                <span title="类型:${fullType}">类型: ${item.type}</span>
                            </div>
                        </div>
                    </div>
                    <button class="item-download-btn" data-img-id="${item.id}" title="下载 ${completedFileName}">下载</button>
                `;
                svgList.appendChild(itemElement);
                
                const itemDownloadBtn = itemElement.querySelector('.item-download-btn');
                itemDownloadBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const imgId = e.currentTarget.dataset.imgId;
                    const imgItem = imageItemCache.get(imgId);
                    if (imgItem) {
                        downloadImage(imgItem, imgItem.originalName, imgItem.originalFormat);
                    }
                });
            });
            setupModalEvents();
        } catch (error) {
            console.error('扫描图片失败:', error);
            svgList.innerHTML = '<div class="loading">扫描失败,请刷新页面重试</div>';
        }
    }

    // 面板事件绑定
    function setupModalEvents() {
        const modal = document.getElementById('svgSnifferModal');
        const closeBtn = modal.querySelector('.close-btn');
        const overlay = document.querySelector('.overlay');
        const selectAllCheckbox = document.getElementById('selectAll');
        const batchDownloadBtn = document.getElementById('batchDownloadBtn');
        const copyBtn = modal.querySelector('.copy-btn');
        
        closeBtn.addEventListener('click', () => {
            modal.style.display = 'none';
            overlay.style.display = 'none';
            cleanupBlobUrls();
        });
        
        overlay.addEventListener('click', () => {
            modal.style.display = 'none';
            overlay.style.display = 'none';
            cleanupBlobUrls();
        });
        
        selectAllCheckbox.addEventListener('change', (e) => {
            const checkboxes = modal.querySelectorAll('.svg-checkbox');
            checkboxes.forEach(checkbox => {
                checkbox.checked = e.target.checked;
            });
        });
        
        batchDownloadBtn.addEventListener('click', () => {
            const checkboxes = modal.querySelectorAll('.svg-checkbox:checked');
            const selectedIds = Array.from(checkboxes).map(cb => cb.dataset.id);
            const selectedItems = selectedIds.map(id => imageItemCache.get(id)).filter(Boolean);
            
            if (selectedItems.length === 0) {
                alert('请至少选择一张图片');
                return;
            }
            
            if (selectedItems.length === 1) {
                const item = selectedItems[0];
                downloadImage(item, item.originalName, item.originalFormat);
            } else {
                downloadMultipleImages(selectedItems);
            }
        });
        
        copyBtn.addEventListener('click', () => {
            const checkboxes = modal.querySelectorAll('.svg-checkbox:checked');
            const selectedIds = Array.from(checkboxes).map(cb => cb.dataset.id);
            const selectedItems = selectedIds.map(id => imageItemCache.get(id)).filter(Boolean);
            
            if (selectedItems.length === 0) {
                alert('请至少选择一张图片');
                return;
            }
            
            const urls = selectedItems.map(item => {
                const completedName = completeImageSuffix(item.originalName || item.name, item.originalFormat);
                return `${completedName}: ${item.url}`;
            }).join('\n');
            GM_setClipboard(urls, 'text');
            
            const notification = document.querySelector('.copy-notification');
            notification.textContent = `已复制 ${selectedItems.length} 个图片链接到剪贴板`;
            notification.style.opacity = '1';
            setTimeout(() => {
                notification.style.opacity = '0';
            }, 2000);
        });
    }

    // 下载函数(含SVG修复+后缀补全)
    function downloadImage(imgItem, originalName, originalFormat) {
        // 1. 处理文件名:补全后缀
        const baseName = originalName || imgItem.name;
        const completedFileName = completeImageSuffix(baseName, originalFormat);
        
        // 2. SVG格式特殊处理
        if (completedFileName.endsWith('.svg') && imgItem.svgContent) {
            try {
                const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>${imgItem.svgContent}`;
                const blob = new Blob([svgContent], { type: 'image/svg+xml;charset=utf-8' });
                saveAs(blob, completedFileName);
                showNotification(`下载成功: ${completedFileName}`, 'success');
                return;
            } catch (svgErr) {
                console.warn('SVG专属下载失败,尝试备用方案:', svgErr);
            }
        }
        
        // 3. 其他格式下载(含补全后缀的文件名)
        const mimeType = completedFileName.endsWith('.svg') 
            ? 'image/svg+xml' 
            : `image/${completedFileName.split('.').pop().toLowerCase()}`;
        
        GM_download({
            url: imgItem.url,
            name: completedFileName,
            mimetype: mimeType,
            onload: () => {
                showNotification(`下载成功: ${completedFileName}`, 'success');
            },
            onerror: (e) => {
                console.error('下载失败:', e);
                showNotification(`下载失败: ${completedFileName}`, 'error');
            }
        });
    }

    // 批量下载函数(含后缀补全)
    async function downloadMultipleImages(selectedItems) {
        const zip = new JSZip();
        let downloadedCount = 0;
        const totalCount = selectedItems.length;
        
        for (const imgItem of selectedItems) {
            try {
                const baseName = imgItem.originalName || imgItem.name;
                const originalFormat = imgItem.originalFormat || '';
                // 补全后缀的文件名
                const completedFileName = completeImageSuffix(baseName, originalFormat);
                
                // SVG特殊处理
                if (completedFileName.endsWith('.svg') && imgItem.svgContent) {
                    const svgContent = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>${imgItem.svgContent}`;
                    zip.file(completedFileName, svgContent);
                    downloadedCount++;
                    continue;
                }
                
                // 其他格式下载
                const response = await fetch(imgItem.url);
                if (!response.ok) throw new Error(`HTTP ${response.status}`);
                const blob = await response.blob();
                zip.file(completedFileName, blob);
                downloadedCount++;
            } catch (error) {
                const errBaseName = imgItem.originalName || imgItem.name;
                const errFileName = completeImageSuffix(`${errBaseName}_加载失败`, 'txt');
                zip.file(errFileName, `图片加载失败: ${imgItem.url}\n错误原因: ${error.message}`);
                console.error(`下载失败 ${errBaseName}:`, error);
            }
        }
        
        if (downloadedCount === 0) {
            alert('所有图片下载失败');
            return;
        }
        
        try {
            const content = await zip.generateAsync({ 
                type: 'blob',
                compression: 'DEFLATE',
                compressionOptions: { level: 6 }
            });
            const zipFileName = `网页图片_${location.hostname}_${new Date().toISOString().slice(0, 10)}.zip`;
            saveAs(content, zipFileName);
            
            showNotification(`批量下载成功: ${zipFileName}(共${downloadedCount}/${totalCount}个)`, 'success');
            if (downloadedCount < totalCount) {
                alert(`部分图片下载失败,成功下载 ${downloadedCount}/${totalCount} 个资源`);
            }
        } catch (error) {
            console.error('创建ZIP失败:', error);
            showNotification('创建ZIP文件失败', 'error');
            alert('创建ZIP文件失败');
        }
    }

    function cleanupBlobUrls() {
        blobUrls.forEach(url => {
            try {
                URL.revokeObjectURL(url);
            } catch (e) {
                console.warn('清理Blob URL失败:', e);
            }
        });
        blobUrls = [];
    }

    function showNotification(message, type = 'info') {
        const colors = {
            info: '#3498db',
            success: '#27ae60',
            warning: '#f39c12',
            error: '#e74c3c'
        };
        
        copyNotification.textContent = message;
        copyNotification.style.backgroundColor = colors[type] || colors.info;
        copyNotification.style.opacity = '1';
        
        setTimeout(() => {
            copyNotification.style.opacity = '0';
        }, 3000);
    }

    initRadarButton();
})();