双击下载当前页面所有图片并打包为压缩包
// ==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('<?xml') || fileContent.includes('<svg')) {
callback('svg')
return
}
callback(null)
})
}
function downloadImag(imgSrc, name, minSize, callback) {
// 使用 GM_xmlhttpRequest 发送跨域请求
GM_xmlhttpRequest({
method: 'GET',
url: imgSrc,
responseType: 'blob', // 确保返回的是 Blob 数据
anonymous: false, // 设置匿名,避免携带 cookies,绕过跨域验证
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', // 模拟 Chrome
'Cache-Control': 'no-cache', // 请求不要缓存
'Pragma': 'no-cache', // HTTP 1.0 的缓存控制(备用)
'Expires': '0', // 设置过期时间为过去时间,强制刷新
'Referer': window.location.origin // 模拟来源页面
},
onload: function(response) {
console.log('response.status', response.status)
if (response.status !== 200) {
console.log('response.response', response.response)
}
// 是否小于最小值
let isSmallerMinSize = false
// 获取图片大小
const contentLength = response.responseHeaders
.split('\r\n')
.find(header => 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()
}
})
})()