// ==UserScript==
// @name 网页图片采集器
// @namespace http://tampermonkey.net/
// @version 4.2
// @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',
fixedFontSize: '10px'
};
GM_addStyle(`
#svgSnifferModal,
#svgSnifferModal * {
font-size: ${CONFIG.fixedFontSize} !important;
line-height: 1.4 !important;
}
.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-weight: 600;}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 18px !important;
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;
}
.action-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.action-btn {
padding: 8px 16px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
white-space: nowrap;
transition: all 0.2s;
}
.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;
/* 新增:锁定预览容器尺寸 */
min-width: ${CONFIG.maxPreviewSize}px;
min-height: ${CONFIG.maxPreviewSize}px;
position: relative;
}
/* 新增:SVG预览专属样式 */
.svg-preview svg {
width: 100% !important;
height: 100% !important;
object-fit: contain !important;
display: block !important;
max-width: ${CONFIG.maxPreviewSize}px !important;
max-height: ${CONFIG.maxPreviewSize}px !important;
}
.svg-preview img {
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;
color: #2c3e50;
margin-bottom: 3px;
max-width: 80px;
}
.svg-meta {
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;
white-space: nowrap;
flex-shrink: 0;
transition: background 0.2s;
}
.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;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 = `
`;
radarContainer.appendChild(radarButton);
document.body.appendChild(radarContainer);
const svgModal = document.createElement('div');
svgModal.id = 'svgSnifferModal';
svgModal.innerHTML = `
`;
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 = [];
// 字节截断工具函数
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;
}
// 文件后缀处理工具函数
function completeImageSuffix(fileName, originalFormat) {
const extMatch = fileName.match(/\.([^.]+)$/);
const hasValidSuffix = extMatch
? CONFIG.supportFormats.includes(extMatch[1].toLowerCase())
: false;
if (hasValidSuffix) return fileName;
const targetFormat = (originalFormat && CONFIG.supportFormats.includes(originalFormat.toLowerCase()))
? originalFormat.toLowerCase()
: CONFIG.defaultImageFormat;
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;
// 移除原始宽高属性,避免限制预览尺寸
svgElement.removeAttribute('width');
svgElement.removeAttribute('height');
// 基于预览容器尺寸设置viewBox(确保缩放基准)
const viewBox = svgElement.getAttribute('viewBox') || `0 0 ${CONFIG.maxPreviewSize} ${CONFIG.maxPreviewSize}`;
svgElement.setAttribute('viewBox', viewBox);
// 强制设置适配样式,确保填充预览容器
svgElement.setAttribute('style', 'width:100%;height:100%;object-fit:contain;');
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;
radarContainer.style.transition = 'none';
}
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('采集
标签失败:', 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 = '正在扫描页面图片资源(含CSS/隐藏元素)...
';
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 = '没有找到任何图片资源(已尝试采集隐藏元素和CSS)
';
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;
const completedFileName = completeImageSuffix(fullName, fullFormat);
let previewHtml = '';
if (item.format === 'svg' && item.svgContent) {
previewHtml = item.svgContent;
} else {
previewHtml = `
`;
}
itemElement.innerHTML = `
${previewHtml}
${truncatedTitle}
格式: ${item.format}
类型: ${item.type}
`;
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 = '扫描失败,请刷新页面重试
';
}
}
// 面板事件绑定
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);
});
}
// 下载函数(含后缀补全)
function downloadImage(imgItem, originalName, originalFormat) {
const baseName = originalName || imgItem.name;
const completedFileName = completeImageSuffix(baseName, originalFormat);
if (completedFileName.endsWith('.svg') && imgItem.svgContent) {
try {
const svgContent = `${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);
}
}
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);
if (completedFileName.endsWith('.svg') && imgItem.svgContent) {
const svgContent = `${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();
})();