- // ==UserScript==
- // @name 网站图片(背景图,svg,canvas)抓取预览下载
- // @namespace https://github.com/yujinpan/tampermonkey-extension
- // @version 2.6
- // @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>
- </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.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);
- };
- }
- })();