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

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

当前为 2021-03-04 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
    };
  }
})();