Greasy Fork is available in English.
将站点所有的图片(背景图,svg,canvas)抓取提供预览,直接点击下载,批量打包下载。
// ==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); }; } })();