// ==UserScript== // @name 双击下载当前页面所有图片并打包为压缩包 // @namespace http://tampermonkey.net/ // @version 0.7 // @description 双击一键下载当前页面所有图片并打包成压缩包,并且可以指定最小图片大小(KB): // @author qingtian1 // @include * // @icon https://www.google.com/s2/favicons?sz=64&domain=bbs.tampermonkey.net.cn // @require https://code.jquery.com/jquery-3.7.1.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @grant GM_xmlhttpRequest // @connect * // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict' // 防止在 iframe 中重复运行代码 if (window.self !== window.top) { console.log('当前页面不是主窗口,无法运行脚本') return // 如果当前不是主窗口,直接退出 } let isDownloading = false // 防止重复触发 function getFormattedDate() { const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') // const hours = String(now.getHours()).padStart(2, '0') // const minutes = String(now.getMinutes()).padStart(2, '0') // const seconds = String(now.getSeconds()).padStart(2, '0') return `${year}-${month}-${day}` // return `${year}-${month}-${day} ${hours}_${minutes}_${seconds}` } function getFormattedDateAndTime() { const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') const hours = String(now.getHours()).padStart(2, '0') const minutes = String(now.getMinutes()).padStart(2, '0') const seconds = String(now.getSeconds()).padStart(2, '0') return `${year}_${month}_${day}_${hours}_${minutes}_${seconds}` } // 创建进度提示框 function createProgressIndicator() { const indicator = document.createElement('div') indicator.id = 'progress-indicator' indicator.style.position = 'fixed' indicator.style.top = '50%' indicator.style.left = '50%' indicator.style.transform = 'translate(-50%, -50%)' indicator.style.padding = '20px 30px' indicator.style.backgroundColor = 'rgba(0, 0, 0, 0.8)' indicator.style.color = '#fff' indicator.style.borderRadius = '8px' indicator.style.fontSize = '18px' indicator.style.zIndex = '9999' indicator.style.display = 'none' indicator.innerHTML = '正在准备下载...' document.body.appendChild(indicator) return indicator } const progressIndicator = createProgressIndicator() function updateProgressIndicator(downloaded, total) { progressIndicator.innerHTML = `下载进度: ${downloaded}/${total}` if (!progressIndicator.style.display || progressIndicator.style.display === 'none') { progressIndicator.style.display = 'block' } } function hideProgressIndicator() { progressIndicator.style.display = 'none' } // 获取文件名 function getFileNameByUrl(url) { const date = getFormattedDate() // console.log('date', date) let fileName if (url.endsWith('.jpg') || url.endsWith('.png') || url.endsWith('.gif') || url.endsWith('.webp') || url.endsWith('.svg')) { fileName = url.replace('http://', '') fileName = fileName.replace('https://', '') fileName = fileName.replaceAll('/', '_') fileName = fileName.substring(0, fileName.lastIndexOf('.')) } else { fileName = Math.random().toString(36).substring(2, 11) } // console.log('fileName', fileName) return `${date}-${fileName}` } function getFileExtensionByContentType(url, contentType, blob, callback) { // 优先从 Content-Type 中获取扩展名 if (contentType) { const mimeMap = { 'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'image/svg+xml': 'svg', 'image/bmp': 'bmp', // BMP格式 'image/x-icon': 'ico', // ICO格式 'image/avif': 'avif', // AVIF格式 'image/heif': 'heif', // HEIF格式 'image/heic': 'heic' // HEIC格式 } const extFromType = mimeMap[contentType.toLowerCase()] if (extFromType) { console.log('从 Content-Type 中获取到的扩展名为:', extFromType) callback(extFromType) return } } // 从 Blob 中解析文件类型 if (contentType !== 'text/html' && blob && blob.arrayBuffer) { getFileExtensionFromBlob(blob, function(extFromType) { if (extFromType) { console.log('从 Blob 中获取到的扩展名为:', extFromType) callback(extFromType) return } // 如果 Blob 中未获取到扩展名,则尝试从 URL 中提取 const urlParts = url.split('?')[0] // 去掉 URL 中的参数 const extMatch = urlParts.match(/\.(\w+)$/) // 匹配最后的 .扩展名 callback(extMatch ? extMatch[1] : 'unknown') // 默认返回 unknown }) return } // 从 URL 中提取扩展名 const urlParts = url.split('?')[0] // 去掉 URL 中的参数 const extMatch = urlParts.match(/\.(\w+)$/) // 匹配最后的 .扩展名 return extMatch ? extMatch[1] : 'unknown' // 默认返回 unknown } function getFileExtensionFromBlob(blob, callback) { // Blob 转为 ArrayBuffer blob.arrayBuffer().then(arrayBuffer => { // 转为 Uint8Array 以读取字节 const uint8Array = new Uint8Array(arrayBuffer) // 读取文件的前4个字节(Magic Number) const magicNumber = uint8Array.slice(0, 4).reduce((hex, byte) => { return hex + byte.toString(16).padStart(2, '0') // 转为16进制字符串 }, '') // Magic Number 对应的文件类型映射 const magicNumbers = { // 图片格式 'ffd8ff': 'jpg', // JPEG '89504e47': 'png', // PNG '47494638': 'gif', // GIF '52494646': 'webp', // WebP '49492a00': 'tif', // TIFF (Intel) '4d4d002a': 'tif', // TIFF (Motorola) '424d': 'bmp', // BMP '00000100': 'ico', // ICO '00000200': 'cur', // CUR (Cursor) '464c4946': 'flif', // FLIF (Free Lossless Image Format) '69636e73': 'icns', // ICNS (Apple Icon Image) '38425053': 'psd' // PSD (Photoshop) } // 返回匹配到的扩展名 for (const [magic, ext] of Object.entries(magicNumbers)) { if (magicNumber.startsWith(magic)) { callback(ext) return } } // 如果没有匹配到,检查是否为 SVG 文件 const textDecoder = new TextDecoder('utf-8') const fileContent = textDecoder.decode(uint8Array) if (fileContent.startsWith(' header.toLowerCase().startsWith('content-length:')) ?.split(':')[1] ?.trim() const sizeKB = contentLength ? parseInt(contentLength, 10) / 1024 : 0 // 转换为 KB // 如果图片小于最小大小,则放入单独文件夹 if (minSize !== 0 && sizeKB < minSize) { console.log(`${name},大小小于 ${minSize}KB`) isSmallerMinSize = true } const contentType = response.responseHeaders .split('\r\n') .find(header => header.toLowerCase().startsWith('content-type:')) ?.split(':')[1] ?.trim() const blob = response.response // 获取文件扩展名 getFileExtensionByContentType(imgSrc, contentType, blob, function(ext) { // 拼接完整文件名 let fullName if (isSmallerMinSize) { // 如果是小于设置值,放入单独文件夹 fullName = `/小于${minSize}KB的图片/${ext}/${name}.${ext}` } else { fullName = `/${ext}/${name}.${ext}` } console.log('fullName', fullName) console.log('Image downloaded successfully!') callback(fullName, blob) }) }, onerror: function(error) { console.error('Failed to fetch image:', error) } }) } function getAllImages() { const images = new Set() // 获取主页面的图片 document.querySelectorAll('img').forEach(img => images.add(img.src)) // 获取所有子 iframe 的图片 document.querySelectorAll('iframe').forEach(iframe => { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document iframeDoc.querySelectorAll('img').forEach(img => images.add(img.src)) } catch (e) { console.warn('无法访问 iframe 的内容:', e) } }) return Array.from(images) } // 下载所有图片 function downloadAllImages() { if (isDownloading) { console.log('正在下载中,请稍候...') return } // 获取当前页面和所有子页面的img元素 const images = getAllImages() if (images.length <= 0) { alert('当前页面无图片,无法下载') return } const defaultMinSizeInput = GM_getValue('downloadMinSize', '0') const minSizeInput = prompt('请输入最小图片大小(KB),0表示无大小限制:', defaultMinSizeInput) // 输入框要求用户输入最小大小 if (minSizeInput === null) { // 用户点击了取消,不执行后续代码 console.log('用户取消了输入大小限制') return } const minSize = parseInt(minSizeInput, 10) if (isNaN(minSize) || minSize < 0) { alert('请输入有效的最小图片大小!') return } GM_setValue('downloadMinSize', minSizeInput) isDownloading = true console.log('开始下载所有图片...') progressIndicator.innerHTML = '正在准备下载...' progressIndicator.style.display = 'block' const totalImages = images.length let downloadedCount = 0 const zip = new JSZip() // 创建一个新的 ZIP 实例 const encodedUrl = encodeURIComponent(window.location.href) let folderName = `images-${encodedUrl}-${getFormattedDateAndTime()}` // 创建文件夹名 if (minSize === 0) { folderName += '_全部图片' } else { folderName += `_大于${minSize}KB的图片` } images.forEach((imageSrc) => { const name = getFileNameByUrl(imageSrc) console.log(`准备下载图片: ${name}`) downloadImag(imageSrc, name, minSize, (fileName, blob) => { // 如果有参数表示需要加入压缩文件 if (fileName && blob) { zip.folder(folderName).file(fileName, blob) // 将图片文件添加到 ZIP 中 } downloadedCount++ console.log(`已下载 ${downloadedCount}/${totalImages} 张图片`) updateProgressIndicator(downloadedCount, totalImages) // 当所有图片下载完成时,恢复下载状态 if (downloadedCount === totalImages) { progressIndicator.innerHTML = '下载完成,正在打包图片...' zip.generateAsync({ type: 'blob' }).then(function(content) { // 生成压缩包文件并提供下载 const objectURL = URL.createObjectURL(content) const a = document.createElement('a') a.href = objectURL a.download = `${folderName}.zip` a.click() console.log('所有图片已打包并下载完成!') progressIndicator.innerHTML = '打包下载完成!' setTimeout(hideProgressIndicator, 2000) isDownloading = false }) } }) }) } function createDownloadButton() { const button = document.createElement('button') button.textContent = '双击下载' button.style.position = 'fixed' button.style.padding = '10px 20px' button.style.backgroundColor = '#007bff' button.style.color = '#fff' button.style.border = 'none' button.style.borderRadius = '5px' button.style.cursor = 'pointer' button.style.boxShadow = '0 2px 5px rgba(0, 0, 0, 0.2)' button.style.zIndex = '9999' // 从 localStorage 加载按钮位置 const savedPosition = GM_getValue('buttonPosition', null) if (savedPosition) { button.style.top = savedPosition.top button.style.left = savedPosition.left } else { button.style.top = '10px' button.style.left = '10px' } // 添加拖拽功能 let isDragging = false let offsetX, offsetY button.addEventListener('mousedown', (e) => { isDragging = true offsetX = e.offsetX offsetY = e.offsetY button.style.cursor = 'grabbing' }) document.addEventListener('mousemove', (e) => { if (isDragging) { const top = `${e.clientY - offsetY}px` const left = `${e.clientX - offsetX}px` button.style.top = top button.style.left = left } }) document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false button.style.cursor = 'pointer' // 保存按钮位置到 localStorage const position = { top: button.style.top, left: button.style.left } GM_setValue('buttonPosition', position) } }) // 点击按钮触发下载 button.addEventListener('dblclick', downloadAllImages) document.body.appendChild(button) } // 初始化按钮 $(function() { createDownloadButton() }) // 添加右键菜单选项 GM_registerMenuCommand('下载该页面的图片并打包', () => { const confirmDownload = confirm('是否下载该页面的所有图片并打包成 ZIP 文件?') if (confirmDownload) { downloadAllImages() } }) })()