// ==UserScript==
// @name 网站图片(背景图,svg,canvas)抓取预览下载
// @namespace https://github.com/yujinpan/tampermonkey-extension
// @version 2.7
// @license MIT
// @description 将站点所有的图片(背景图,svg,canvas)抓取提供预览,直接点击下载,批量打包下载。
// @author yujinpan
// @include http*://**
// @require https://cdn.bootcss.com/jszip/3.2.2/jszip.min.js
// @run-at document-start
// ==/UserScript==
/**
* 已有功能列表:
* - 抓取页面上的图片链接,包括 **img,背景图,svg,canvas**
* - 提供展示抓取的图片的列表快速预览
* - 提供按钮快速切换抓取的图片展示区
* - 提供快速下载,点击预览即可下载源图片文件
* - 提供动态抓取后来加载的图片
*
* 2021-03-04
* - 优化查找性能,平衡每次查找的数量与间隔
* - 修复部分元素查找失败问题
* - 修复被破坏的原生代码
*
* 2020-3-30
* - 新增对【图片尺寸过滤】设置的记住功能
* - 优化查询速度
*
* 2020-1-12
* - 新增【图片尺寸过滤】功能
* - 修复会出现重复的情况
*
* 2019-12-23
* - 修复 blob 类型的图片展示与下载失败问题
* - 优化性能,解决多图的卡顿问题
*
* 2019-11-17 更新内容:
* - **新增【批量下载功能】一键打包下载全部图片**
*
* 2019-5-17 更新内容:
* - 修复 svg,canvas 展示与下载问题
* - 增加暗黑透明样式,黑色,白色图片区分明显
* - 重构核心代码,分模块执行,提高可读性与维护性
* - 兼容 iframe 的 btoa 方法报错
*/
(() => {
// 存放抓取与生成的图片
const urls = new Set();
const blobUrls = new Set();
let timeId;
// 开启高级模式
advance();
// 初始化
document.addEventListener('DOMContentLoaded', init);
/**
* 初始化
*/
function init() {
// 创建样式
createStyle();
// 创建容器
const section = document.createElement('section');
section.id = 'SIR';
section.innerHTML = `
<button class="SIR-toggle-button SIR-button">自动获取图片</button>
<div class="SIR-cover"></div>
<div class="SIR-main-wrap">
<ul class="SIR-main">
</ul>
<div class="SIR-tools">
<select class="SIR-filter-mini-button SIR-button">
<option value ="0">不进行过滤</option>
<option value ="50">宽高大于 50</option>
<option value ="100">宽高大于 100</option>
<option value="150">宽高大于 150</option>
<option value="200">宽高大于 200</option>
<option value="300">宽高大于 300</option>
<option value="400">宽高大于 400</option>
<option value="600">宽高大于 600</option>
<option value="800">宽高大于 800</option>
<option value="1000">宽高大于 1000</option>
</select>
<button class="SIR-download-bat-button SIR-button">批量下载(部分可能失败)</button>
</div>
<div class="SIR-download-program"></div>
</div>
`;
document.body.append(section);
// 获取按钮与列表 DOM
const button = section.querySelector('.SIR-toggle-button');
const main = section.querySelector('.SIR-main');
const downloadBat = section.querySelector('.SIR-download-bat-button');
const filterMini = section.querySelector('.SIR-filter-mini-button');
// 切换时进行抓取
let showMain = false;
const reset = () => {
main.innerHTML = '';
urls.clear();
blobUrls.clear();
clearTimeout(timeId);
};
const initImages = () => {
imagesReptile(url => {
!urls.has(url) && main.appendChild(addListItem(url));
});
};
button.onclick = () => {
showMain = !showMain;
reset();
if (showMain) {
initImages();
}
section.classList.toggle('active', showMain);
};
downloadBat.onclick = downloadAll;
// filter
const filter = localStorage.getItem('SIR_FILTER');
filter && (filterMini.value = filter);
filterMini.onchange = (e) => {
localStorage.setItem('SIR_FILTER', e.target.value);
reset();
initImages();
};
}
/**
* 添加图片列表项
* @param {String} url
* @return {HTMLLIElement}
*/
function addListItem(url) {
urls.add(url);
let li, a, img;
li = document.createElement('li');
a = document.createElement('a');
img = document.createElement('img');
a.target = '_blank';
a.download = 'image';
a.title = '点击下载';
a.href = url;
img.src = url;
a.appendChild(img);
li.appendChild(a);
return li;
}
/**
* 获取资源列表
* @param {Function} callback 参数为 url 值
*/
function imagesReptile(callback) {
const elements = Array.from(document.querySelectorAll(`
*:not(head):not(script):not(textarea):not(input):not(meta):not(title):not(style):not(link)
`));
const elem = document.querySelector('.SIR-download-program');
elem.classList.add('active');
elem.innerHTML = getProgramHTML(0, elements.length);
let url, index = 0, element, len = elements.length, tagName,
filterMiniSize = +document.querySelector('.SIR-filter-mini-button').value;
// 遍历取出 img,backgroundImage,svg,canvas
(function each() {
element = elements[index];
// 过滤小图尺寸
if (
(filterMiniSize && element.clientWidth > filterMiniSize && element.clientHeight > filterMiniSize) ||
!filterMiniSize
) {
tagName = element.tagName.toLowerCase();
url = '';
// img 标签
if (tagName === 'img') {
try {
url = getImgUrl(element);
} catch (e) {
warnMessage(e);
}
}
// svg
else if (tagName === 'svg') {
try {
url = getSvgImage(element);
} catch (e) {
warnMessage(e);
}
}
// canvas
else if (tagName === 'canvas') {
try {
url = getCanvasImage(element);
} catch (e) {
warnMessage(e);
}
}
// background-image
else {
const backgroundImage = getComputedStyle(element).backgroundImage;
if (backgroundImage !== 'none' && backgroundImage.startsWith('url')) {
url = backgroundImage.slice(5, -2);
}
}
}
url && callback(url);
elem.innerHTML = getProgramHTML(index + 1, elements.length);
if (++index < len) {
// 延迟计算(解决卡顿问题)
// 每进行 50 次计算就休息一次
if (Number.isInteger(index / 50)) {
timeId = setTimeout(() => each(), 0);
} else {
each();
}
} else {
elem.classList.remove('active');
}
})();
}
/**
* 创建样式
*/
function createStyle() {
const style = document.createElement('style');
style.innerHTML = `
#SIR * {
box-sizing: border-box;
padding: 0;
margin: 0;
}
#SIR.active .SIR-cover {
display: block;
}
#SIR.active .SIR-main-wrap {
display: block;
}
#SIR .SIR-button {
visibility: visible;
display: inline-block;
height: 22px;
line-height: 22px;
margin-right: 10px;
padding: 0 3px;
opacity: 0.5;
background: white;
font-size: 13px;
}
#SIR .SIR-button:hover {
opacity: 1;
}
#SIR .SIR-toggle-button {
position: fixed;
right: 0;
bottom: 0;
z-index: 99999;
}
#SIR .SIR-cover,
#SIR .SIR-main-wrap {
display: none;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
#SIR .SIR-cover {
z-index: 99997;
background: rgba(255, 255, 255, 0.7);
}
#SIR .SIR-main-wrap {
z-index: 99998;
overflow-y: auto;
background: rgba(0, 0, 0, 0.7);
}
#SIR .SIR-main {
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
list-style-type: none;
}
#SIR .SIR-main > li {
box-sizing: border-box;
width: 10%;
min-width: 168px;
min-height: 100px;
max-height: 200px;
border: 2px solid transparent;
box-shadow: 0 0 1px 1px white;
background: rgba(0, 0, 0, 0.5);
overflow: hidden;
}
#SIR .SIR-main > li > a {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
#SIR .SIR-main > li:hover img {
transform: scale(1.5);
}
#SIR .SIR-main > li img {
transition: transform .3s;
max-width: 100%;
}
#SIR .SIR-tools {
position: fixed;
bottom: 0;
right: 100px;
display: flex;
}
#SIR .SIR-download-program {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: white;
background-color: inherit;
border: 1px solid white;
font-size: 20px;
display: none;
}
#SIR .SIR-download-program.active {
display: flex;
}
`;
document.head.append(style);
}
/**
* 获取 svg 图片链接
* @param {Element} svg svg 元素
*/
function getSvgImage(svg) {
svg.setAttribute('version', '1.1');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
try {
return 'data:image/svg+xml;base64,' + btoa(svg.outerHTML);
} catch (e) {
warnMessage('svg创建失败');
return '';
}
}
/**
* 获取 canvas 图片链接
* @param {HTMLCanvasElement} canvas canvas 元素
*/
function getCanvasImage(canvas) {
return canvas.toDataURL_();
}
/**
* 获取 img 的链接
* @description
* 兼容 srcset 属性
* @param {HTMLImageElement} element 图片元素
*/
function getImgUrl(element) {
let url;
// 兼容 srcset 属性
if (element.srcset) {
const srcs = element.srcset.split(' ');
url = srcs[0];
} else {
url = element.src;
// blob 类型可能被 revoke,这里生成 canvas
if (!blobUrls.has(url) && url.startsWith('blob')) {
blobUrls.add(url); // 存储源地址用于判断是否已经生成,因为生成的已经转换了
const canvas = createCanvasWithImg(element);
url = getCanvasImage(canvas);
}
}
return url;
}
/**
* 创建 img 元素的 canvas
* @param {HTMLImageElement} imgElem
*/
function createCanvasWithImg(imgElem) {
const canvas = document.createElement('canvas');
canvas.width = imgElem.naturalWidth || imgElem.width;
canvas.height = imgElem.naturalHeight || imgElem.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(imgElem, 0, 0);
return canvas;
}
/**
* 获取链接的图片文件
* @param url
* @return {Promise<{file, suffix}>}
*/
function getImg(url) {
return new Promise((resolve) => {
// 如果是链接,就先加载图片,再存文件
if (/((\.(png|jpg|jpeg|gif|svg)$)|^(http|\/|file|blob))/.test(url)) {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'blob';
request.onload = function () {
let suffix = url.match(/\.[a-zA-Z]+$/);
suffix = suffix ? suffix[0] : '.png';
resolve({file: request.response, suffix});
};
request.onerror = function (e) {
warnMessage('图片获取失败', url, e);
resolve(null);
};
request.send();
} else if (url.includes('base64')) {
let suffix = '.' + url.replace('data:image/', '').match(/^[a-zA-Z]*/)[0];
resolve({
file: dataURLtoFile(url, 'image' + suffix),
suffix
});
} else {
warnMessage('图片类型无法解析,请联系插件作者', url);
resolve(null);
}
});
}
/**
* 将 base64 转换为文件
* @param dataUrl
* @param filename
* @return {File}
*/
function dataURLtoFile(dataUrl, filename) {
let arr = dataUrl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, {type: mime});
}
/**
* 批量下载所有文件
*/
function downloadAll() {
const elem = document.querySelector('.SIR-download-program');
if (elem && !elem.classList.contains('active')) {
let total = 0;
let successCount = 0;
const promiseArr = Array.from(urls).map((item) => {
return getImg(item).then(res => {
successCount++;
elem.innerHTML = getProgramHTML(successCount, total);
return res;
});
});
total = promiseArr.length;
if (total) {
elem.classList.add('active');
elem.innerHTML = getProgramHTML(successCount, total);
Promise.all(promiseArr).then(res => {
res = res.filter(item => item);
const zip = new JSZip();
res.forEach((item, index) => zip.file('image' + index + item.suffix, item.file));
zip.generateAsync({type: 'blob'})
.then(function (blob) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.download = 'images.zip';
a.href = url;
a.click();
elem.classList.remove('active');
URL.revokeObjectURL(url);
});
}, () => {
alert('下载失败');
elem.classList.remove('active');
});
} else {
alert('暂无图片');
}
}
}
/**
* 获取下载进度 HTML
* @param program
* @param total
* @return {string}
*/
function getProgramHTML(program, total) {
return `<b>${program}</b> / ${total}`;
}
/**
* 警告信息
* @param params
*/
function warnMessage(...params) {
console.warn('[自动获取图片]:', ...params);
}
function advance() {
// `toDataURL` was broke
HTMLCanvasElement.prototype.toDataURL_ = HTMLCanvasElement.prototype.toDataURL;
// remove tainted source
const canvasContextPrototype = CanvasRenderingContext2D.prototype;
canvasContextPrototype.drawImage_ = CanvasRenderingContext2D.prototype.drawImage;
canvasContextPrototype.drawImage = function () {
const { src, crossOrigin } = arguments[0];
if (src.startsWith('http') && location.origin !== src.slice(0, src.indexOf('/', 8)) && !crossOrigin) {
console.log('%c 【自动获取图片】站点正在加载无法下载的图片,请自行访问该链接下载:', 'color: orange;', src);
return;
}
this.drawImage_.apply(this, arguments);
};
}
})();