网站图片(背景图,svg,canvas)抓取预览下载

将站点所有的图片(背景图,svg,canvas)抓取提供预览,直接点击下载,批量打包下载。

As of 2021-03-04. See the latest version.

  1. // ==UserScript==
  2. // @name 网站图片(背景图,svg,canvas)抓取预览下载
  3. // @namespace https://github.com/yujinpan/tampermonkey-extension
  4. // @version 2.6
  5. // @license MIT
  6. // @description 将站点所有的图片(背景图,svg,canvas)抓取提供预览,直接点击下载,批量打包下载。
  7. // @author yujinpan
  8. // @include http*://**
  9. // @require https://cdn.bootcss.com/jszip/3.2.2/jszip.min.js
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. /**
  14. * 已有功能列表:
  15. * - 抓取页面上的图片链接,包括 **img,背景图,svg,canvas**
  16. * - 提供展示抓取的图片的列表快速预览
  17. * - 提供按钮快速切换抓取的图片展示区
  18. * - 提供快速下载,点击预览即可下载源图片文件
  19. * - 提供动态抓取后来加载的图片
  20. *
  21. * 2021-03-04
  22. * - 优化查找性能,平衡每次查找的数量与间隔
  23. * - 修复部分元素查找失败问题
  24. * - 修复被破坏的原生代码
  25. *
  26. * 2020-3-30
  27. * - 新增对【图片尺寸过滤】设置的记住功能
  28. * - 优化查询速度
  29. *
  30. * 2020-1-12
  31. * - 新增【图片尺寸过滤】功能
  32. * - 修复会出现重复的情况
  33. *
  34. * 2019-12-23
  35. * - 修复 blob 类型的图片展示与下载失败问题
  36. * - 优化性能,解决多图的卡顿问题
  37. *
  38. * 2019-11-17 更新内容:
  39. * - **新增【批量下载功能】一键打包下载全部图片**
  40. *
  41. * 2019-5-17 更新内容:
  42. * - 修复 svg,canvas 展示与下载问题
  43. * - 增加暗黑透明样式,黑色,白色图片区分明显
  44. * - 重构核心代码,分模块执行,提高可读性与维护性
  45. * - 兼容 iframe 的 btoa 方法报错
  46. */
  47.  
  48. (() => {
  49. // 存放抓取与生成的图片
  50. const urls = new Set();
  51. const blobUrls = new Set();
  52. let timeId;
  53.  
  54. // 开启高级模式
  55. advance();
  56.  
  57. // 初始化
  58. document.addEventListener('DOMContentLoaded', init);
  59.  
  60. /**
  61. * 初始化
  62. */
  63. function init() {
  64. // 创建样式
  65. createStyle();
  66.  
  67. // 创建容器
  68. const section = document.createElement('section');
  69. section.id = 'SIR';
  70. section.innerHTML = `
  71. <button class="SIR-toggle-button SIR-button">自动获取图片</button>
  72. <div class="SIR-cover"></div>
  73. <div class="SIR-main-wrap">
  74. <ul class="SIR-main">
  75. </ul>
  76. <div class="SIR-tools">
  77. <select class="SIR-filter-mini-button SIR-button">
  78. <option value ="0">不进行过滤</option>
  79. <option value ="50">宽高大于 50</option>
  80. <option value ="100">宽高大于 100</option>
  81. <option value="150">宽高大于 150</option>
  82. <option value="200">宽高大于 200</option>
  83. </select>
  84. <button class="SIR-download-bat-button SIR-button">批量下载</button>
  85. </div>
  86. <div class="SIR-download-program"></div>
  87. </div>
  88. `;
  89. document.body.append(section);
  90.  
  91. // 获取按钮与列表 DOM
  92. const button = section.querySelector('.SIR-toggle-button');
  93. const main = section.querySelector('.SIR-main');
  94. const downloadBat = section.querySelector('.SIR-download-bat-button');
  95. const filterMini = section.querySelector('.SIR-filter-mini-button');
  96.  
  97. // 切换时进行抓取
  98. let showMain = false;
  99.  
  100. const reset = () => {
  101. main.innerHTML = '';
  102. urls.clear();
  103. blobUrls.clear();
  104. clearTimeout(timeId);
  105. };
  106.  
  107. const initImages = () => {
  108. imagesReptile(url => {
  109. !urls.has(url) && main.appendChild(addListItem(url));
  110. });
  111. };
  112.  
  113. button.onclick = () => {
  114. showMain = !showMain;
  115. reset();
  116. if (showMain) {
  117. initImages();
  118. }
  119. section.classList.toggle('active', showMain);
  120. };
  121. downloadBat.onclick = downloadAll;
  122.  
  123. // filter
  124. const filter = localStorage.getItem('SIR_FILTER');
  125. filter && (filterMini.value = filter);
  126. filterMini.onchange = (e) => {
  127. localStorage.setItem('SIR_FILTER', e.target.value);
  128. reset();
  129. initImages();
  130. };
  131. }
  132.  
  133. /**
  134. * 添加图片列表项
  135. * @param {String} url
  136. * @return {HTMLLIElement}
  137. */
  138. function addListItem(url) {
  139. urls.add(url);
  140.  
  141. let li, a, img;
  142. li = document.createElement('li');
  143. a = document.createElement('a');
  144. img = document.createElement('img');
  145.  
  146. a.download = 'image';
  147. a.title = '点击下载';
  148. a.href = url;
  149. img.src = url;
  150.  
  151. a.appendChild(img);
  152. li.appendChild(a);
  153.  
  154. return li;
  155. }
  156.  
  157. /**
  158. * 获取资源列表
  159. * @param {Function} callback 参数为 url 值
  160. */
  161. function imagesReptile(callback) {
  162. const elements = Array.from(document.querySelectorAll(`
  163. *:not(head):not(script):not(textarea):not(input):not(meta):not(title):not(style):not(link)
  164. `));
  165. const elem = document.querySelector('.SIR-download-program');
  166. elem.classList.add('active');
  167. elem.innerHTML = getProgramHTML(0, elements.length);
  168.  
  169. let url, index = 0, element, len = elements.length, tagName,
  170. filterMiniSize = +document.querySelector('.SIR-filter-mini-button').value;
  171. // 遍历取出 img,backgroundImage,svg,canvas
  172. (function each() {
  173. element = elements[index];
  174.  
  175. // 过滤小图尺寸
  176. if (
  177. (filterMiniSize && element.clientWidth > filterMiniSize && element.clientHeight > filterMiniSize) ||
  178. !filterMiniSize
  179. ) {
  180. tagName = element.tagName.toLowerCase();
  181. url = '';
  182.  
  183. // img 标签
  184. if (tagName === 'img') {
  185. try {
  186. url = getImgUrl(element);
  187. } catch (e) {
  188. warnMessage(e);
  189. }
  190. }
  191. // svg
  192. else if (tagName === 'svg') {
  193. try {
  194. url = getSvgImage(element);
  195. } catch (e) {
  196. warnMessage(e);
  197. }
  198. }
  199. // canvas
  200. else if (tagName === 'canvas') {
  201. try {
  202. url = getCanvasImage(element);
  203. } catch (e) {
  204. warnMessage(e);
  205. }
  206. }
  207. // background-image
  208. else {
  209. const backgroundImage = getComputedStyle(element).backgroundImage;
  210. if (backgroundImage !== 'none' && backgroundImage.startsWith('url')) {
  211. url = backgroundImage.slice(5, -2);
  212. }
  213. }
  214. }
  215.  
  216. url && callback(url);
  217.  
  218. elem.innerHTML = getProgramHTML(index + 1, elements.length);
  219.  
  220. if (++index < len) {
  221. // 延迟计算(解决卡顿问题)
  222. // 每进行 50 次计算就休息一次
  223. if (Number.isInteger(index / 50)) {
  224. timeId = setTimeout(() => each(), 0);
  225. } else {
  226. each();
  227. }
  228. } else {
  229. elem.classList.remove('active');
  230. }
  231. })();
  232. }
  233.  
  234. /**
  235. * 创建样式
  236. */
  237. function createStyle() {
  238. const style = document.createElement('style');
  239. style.innerHTML = `
  240. #SIR * {
  241. box-sizing: border-box;
  242. padding: 0;
  243. margin: 0;
  244. }
  245. #SIR.active .SIR-cover {
  246. display: block;
  247. }
  248. #SIR.active .SIR-main-wrap {
  249. display: block;
  250. }
  251. #SIR .SIR-button {
  252. visibility: visible;
  253. display: inline-block;
  254. height: 22px;
  255. line-height: 22px;
  256. margin-right: 10px;
  257. padding: 0 3px;
  258. opacity: 0.5;
  259. background: white;
  260. font-size: 13px;
  261. }
  262. #SIR .SIR-button:hover {
  263. opacity: 1;
  264. }
  265. #SIR .SIR-toggle-button {
  266. position: fixed;
  267. right: 0;
  268. bottom: 0;
  269. z-index: 99999;
  270. }
  271. #SIR .SIR-cover,
  272. #SIR .SIR-main-wrap {
  273. display: none;
  274. position: fixed;
  275. width: 100%;
  276. height: 100%;
  277. top: 0;
  278. left: 0;
  279. }
  280. #SIR .SIR-cover {
  281. z-index: 99997;
  282. background: rgba(255, 255, 255, 0.7);
  283. }
  284. #SIR .SIR-main-wrap {
  285. z-index: 99998;
  286. overflow-y: auto;
  287. background: rgba(0, 0, 0, 0.7);
  288. }
  289. #SIR .SIR-main {
  290. margin: 0;
  291. padding: 0;
  292. display: flex;
  293. flex-wrap: wrap;
  294. list-style-type: none;
  295. }
  296. #SIR .SIR-main > li {
  297. box-sizing: border-box;
  298. width: 10%;
  299. min-width: 168px;
  300. min-height: 100px;
  301. max-height: 200px;
  302. border: 2px solid transparent;
  303. box-shadow: 0 0 1px 1px white;
  304. background: rgba(0, 0, 0, 0.5);
  305. overflow: hidden;
  306. }
  307. #SIR .SIR-main > li > a {
  308. display: flex;
  309. justify-content: center;
  310. align-items: center;
  311. width: 100%;
  312. height: 100%;
  313. }
  314. #SIR .SIR-main > li:hover img {
  315. transform: scale(1.5);
  316. }
  317. #SIR .SIR-main > li img {
  318. transition: transform .3s;
  319. max-width: 100%;
  320. }
  321. #SIR .SIR-tools {
  322. position: fixed;
  323. bottom: 0;
  324. right: 100px;
  325. display: flex;
  326. }
  327. #SIR .SIR-download-program {
  328. position: fixed;
  329. top: 0;
  330. left: 0;
  331. width: 100%;
  332. height: 100%;
  333. display: flex;
  334. align-items: center;
  335. justify-content: center;
  336. color: white;
  337. background-color: inherit;
  338. border: 1px solid white;
  339. font-size: 20px;
  340. display: none;
  341. }
  342. #SIR .SIR-download-program.active {
  343. display: flex;
  344. }
  345. `;
  346. document.head.append(style);
  347. }
  348.  
  349. /**
  350. * 获取 svg 图片链接
  351. * @param {Element} svg svg 元素
  352. */
  353. function getSvgImage(svg) {
  354. svg.setAttribute('version', '1.1');
  355. svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  356.  
  357. try {
  358. return 'data:image/svg+xml;base64,' + btoa(svg.outerHTML);
  359. } catch (e) {
  360. warnMessage('svg创建失败');
  361. return '';
  362. }
  363. }
  364.  
  365. /**
  366. * 获取 canvas 图片链接
  367. * @param {HTMLCanvasElement} canvas canvas 元素
  368. */
  369. function getCanvasImage(canvas) {
  370. return canvas.toDataURL_();
  371. }
  372.  
  373. /**
  374. * 获取 img 的链接
  375. * @description
  376. * 兼容 srcset 属性
  377. * @param {HTMLImageElement} element 图片元素
  378. */
  379. function getImgUrl(element) {
  380. let url;
  381.  
  382. // 兼容 srcset 属性
  383. if (element.srcset) {
  384. const srcs = element.srcset.split(' ');
  385. url = srcs[0];
  386. } else {
  387. url = element.src;
  388. // blob 类型可能被 revoke,这里生成 canvas
  389. if (!blobUrls.has(url) && url.startsWith('blob')) {
  390. blobUrls.add(url); // 存储源地址用于判断是否已经生成,因为生成的已经转换了
  391. const canvas = createCanvasWithImg(element);
  392. url = getCanvasImage(canvas);
  393. }
  394. }
  395.  
  396. return url;
  397. }
  398.  
  399. /**
  400. * 创建 img 元素的 canvas
  401. * @param {HTMLImageElement} imgElem
  402. */
  403. function createCanvasWithImg(imgElem) {
  404. const canvas = document.createElement('canvas');
  405. canvas.width = imgElem.naturalWidth || imgElem.width;
  406. canvas.height = imgElem.naturalHeight || imgElem.height;
  407. const ctx = canvas.getContext('2d');
  408. ctx.drawImage(imgElem, 0, 0);
  409. return canvas;
  410. }
  411.  
  412. /**
  413. * 获取链接的图片文件
  414. * @param url
  415. * @return {Promise<{file, suffix}>}
  416. */
  417. function getImg(url) {
  418. return new Promise((resolve) => {
  419. // 如果是链接,就先加载图片,再存文件
  420. if (/((\.(png|jpg|jpeg|gif|svg)$)|^(http|\/|file|blob))/.test(url)) {
  421. const request = new XMLHttpRequest();
  422. request.open('GET', url, true);
  423. request.responseType = 'blob';
  424. request.onload = function () {
  425. let suffix = url.match(/\.[a-zA-Z]+$/);
  426. suffix = suffix ? suffix[0] : '.png';
  427. resolve({file: request.response, suffix});
  428. };
  429. request.onerror = function (e) {
  430. warnMessage('图片获取失败', url, e);
  431. resolve(null);
  432. };
  433. request.send();
  434. } else if (url.includes('base64')) {
  435. let suffix = '.' + url.replace('data:image/', '').match(/^[a-zA-Z]*/)[0];
  436. resolve({
  437. file: dataURLtoFile(url, 'image' + suffix),
  438. suffix
  439. });
  440. } else {
  441. warnMessage('图片类型无法解析,请联系插件作者', url);
  442. resolve(null);
  443. }
  444. });
  445. }
  446.  
  447. /**
  448. * 将 base64 转换为文件
  449. * @param dataUrl
  450. * @param filename
  451. * @return {File}
  452. */
  453. function dataURLtoFile(dataUrl, filename) {
  454. let arr = dataUrl.split(','),
  455. mime = arr[0].match(/:(.*?);/)[1],
  456. bstr = atob(arr[1]),
  457. n = bstr.length,
  458. u8arr = new Uint8Array(n);
  459. while (n--) {
  460. u8arr[n] = bstr.charCodeAt(n);
  461. }
  462. return new File([u8arr], filename, {type: mime});
  463. }
  464.  
  465. /**
  466. * 批量下载所有文件
  467. */
  468. function downloadAll() {
  469. const elem = document.querySelector('.SIR-download-program');
  470. if (elem && !elem.classList.contains('active')) {
  471. let total = 0;
  472. let successCount = 0;
  473. const promiseArr = Array.from(urls).map((item) => {
  474. return getImg(item).then(res => {
  475. successCount++;
  476. elem.innerHTML = getProgramHTML(successCount, total);
  477. return res;
  478. });
  479. });
  480. total = promiseArr.length;
  481. if (total) {
  482. elem.classList.add('active');
  483. elem.innerHTML = getProgramHTML(successCount, total);
  484. Promise.all(promiseArr).then(res => {
  485. res = res.filter(item => item);
  486. const zip = new JSZip();
  487. res.forEach((item, index) => zip.file('image' + index + item.suffix, item.file));
  488. zip.generateAsync({type: 'blob'})
  489. .then(function (blob) {
  490. const url = URL.createObjectURL(blob);
  491. const a = document.createElement('a');
  492. a.download = 'images.zip';
  493. a.href = url;
  494. a.click();
  495. elem.classList.remove('active');
  496. URL.revokeObjectURL(url);
  497. });
  498. }, () => {
  499. alert('下载失败');
  500. elem.classList.remove('active');
  501. });
  502. } else {
  503. alert('暂无图片');
  504. }
  505. }
  506. }
  507.  
  508. /**
  509. * 获取下载进度 HTML
  510. * @param program
  511. * @param total
  512. * @return {string}
  513. */
  514. function getProgramHTML(program, total) {
  515. return `<b>${program}</b> / ${total}`;
  516. }
  517.  
  518. /**
  519. * 警告信息
  520. * @param params
  521. */
  522. function warnMessage(...params) {
  523. console.warn('[自动获取图片]:', ...params);
  524. }
  525.  
  526. function advance() {
  527. // `toDataURL` was broke
  528. HTMLCanvasElement.prototype.toDataURL_ = HTMLCanvasElement.prototype.toDataURL;
  529.  
  530. // remove tainted source
  531. const canvasContextPrototype = CanvasRenderingContext2D.prototype;
  532. canvasContextPrototype.drawImage_ = CanvasRenderingContext2D.prototype.drawImage;
  533. canvasContextPrototype.drawImage = function () {
  534. const { src, crossOrigin } = arguments[0];
  535. if (src.startsWith('http') && location.origin !== src.slice(0, src.indexOf('/', 8)) && !crossOrigin) {
  536. console.log('%c 【自动获取图片】站点正在加载无法下载的图片,请自行访问该链接下载:', 'color: orange;', src);
  537. return;
  538. }
  539. this.drawImage_.apply(this, arguments);
  540. };
  541. }
  542. })();