使用svg进行网页截图

前言

很多时候,在业务中,我们可能要把整个网页,或者网页中的一部分转化成图片,从而达到一些业务需求,或者营销目的。在以往,我们通常会用html2canvas来完成这个操作。虽然可以满足大部分的需求,但是还是有些不足,比如 计算精度缺失,box-shadow的支持不好 等。

为了尝试解决这些问题,我们可以尝试使用svg来绘制html。对于svg是一种可扩展标记语言,在转化的过程中,就需要使用到 <foreignObject> 这个svg元素。 <foreignObject> 允许包含不同的XML命名空间,在浏览器的上下文中,很可能是XHTML或者HTML。

<foreignObject>介绍

关于 <foreignObject> 的简单用法,可以这样简单描述一下

const html = document.createElement('html');
const str = `
  <head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
      #box {
          height: 200px;
          width: 200px;
          background: red;
      }
    </style>
  </head>
  <body>
    <div id="div">
      <div id="box"></div>
    </div>
  </body>
`;
html.innerHTML = str;
// 序列化html片段,转化成 foreignObject 可以使用的格式
const xhtml = new XMLSerializer().serializeToString(html);
return (
  `
    <svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
      <foreignObject x="0" y="0" width="300" height="300">
        ${xhtml}
      </foreignObject>
    </svg>
  `
)
复制代码

生成的svg图片如下

image.png

准备工作

现实场景中的页面肯定没有上面的那么简单,写个稍微复杂的页面,来用作示例。

[html文件]
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <link rel="stylesheet" href="https://juejin.cn/post/main.css">
  <style>
    * {
        padding: 0;
        margin: 0;
    }
    img {
        vertical-align: middle;
    }
    #container {
        width: 300px;
        height: 500px;
        margin: 48px 0 0 48px;
        box-shadow: 0 0 16px rgba(0, 0, 0, .4);
        border-radius: 6px;
        padding: 24px 16px;
        box-sizing: border-box;
    }
  </style>
</head>
<body>
  <div id="container">
    <div class="img-header">
      <img
        width="100"
        height="100"
        src="https://huijian-mini-oss.zhidianfan.com/img/logo-alt.png"
        
      >
    </div>
    <div class="content">
      <div class="name">
        懒狗小前端
      </div>
      <div class="section">
        阿宾的高中成绩并不理想,但是毕竟也给他考上了台北附近的一所私立专校。开学之前,
        他考虑到每天通车恐怕太过于辛苦,于是就在学校旁边租了间学生房,
        只有周末假日,才回家看看妈妈。
      </div>
    </div>
  </div>
  <div style="margin-top: 16px;text-align: center;width: 300px;padding-left: 48px;">
    <button id="btn">我要截图啦</button>
  </div>
  <script src="https://juejin.cn/post/main.js"></script>
</body>
</html>
复制代码
[main.css]
.img-header {
    text-align: center;
}
.name {
    text-align: center;
    margin-top: 24px;
    font-size: 20px;
    line-height: 28px;
    font-weight: bold;
    color: #333;
    transform: rotate(-30deg);
}

.section {
    font-size: 14px;
    line-height: 22px;
    margin-top: 24px;
    text-indent: 2rem;
    background: #00a0e9;
    padding: 12px;
    border-radius: 6px;
    color: #fff;
}
复制代码
[main.js]
const div = document.querySelector('.section');
let txt = div.innerText;
txt += `巴啦啦小魔仙变身了哇。`

div.innerText = txt;
复制代码

页面展示如下:

image.png

开始

一、

首先,和html2canvas一样,如果图片是外链,我们需要转化成base64才能在svg中正常展示。

const replaceImgSrc = function (doc) {
  return new Promise(resolve => {
    // 获取所有的图片
    const imgs = Array.from(doc.getElementsByTagName('img'));
    const length = imgs.filter(v => v.src).length;
    // 完成的img计数器
    let num = 0;
    // 使用canvas把img转化成base64
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    // 把img转为base64,替换img的src
    const replaceSrc = function (img) {
      const image = new Image();
      image.setAttribute('crossOrigin', 'anonymous');
      image.src = img.src;
      image.onload = function () {
        canvas.width = this.width;
        canvas.height = this.height
        ctx.drawImage(this, 0, 0);
        const data = canvas.toDataURL('image/png');
        img.src = data;
        num++;
        checkReplace();
      }
    }
    // 检查是否所有图片加载完成,替换完成。
    const checkReplace = function () {
      if (num === length) {
        resolve(doc);
      }
    }
    // 遍历imgs
    for (let img of imgs) {
      replaceSrc(img);
    }
  })
}
复制代码

二、

然后,<foreignObject>中也不支持外链的css,我们需要把外链的css转换成inline的style。

// 读取外链的css
const getOuterLink = function (doc) {
  // 找到外链
  const head = doc.querySelector('head');
  const links = Array.from(doc.getElementsByTagName('link'));

  // 读取单个css文件
  const getCss = function (l) {
    const url = l.href;
    return fetch(url)
      .then(res => {
        head.removeChild(l);
        return res.text();
      });
  }

  // 读取全部的css文件
  const getAllCss = function () {
    const arr = [];
    for (let l of links) {
      arr.push(getCss(l));
    }
    return Promise.all(arr)
      .then(res => {
        for (let s of res) {
          const style = doc.createElement('style');
          style.innerHTML = s;
          head.appendChild(style);
        }
      });
  }
  return getAllCss();
}
复制代码

三、

因为在修改的同时,我们不能修改原文档中的内容,所以在修改src和css之前,我们得新创建一个document,用来进行这些操作,之所以放在这,因为写在第一步,也没啥意义,因为写最前面也不知道要用来干啥

// 创建一个document副本来进行css img的替换操作
const createDocument = function () {
  // 获取html中的内容
  const html = document.getElementsByTagName('html')[0];
  const htmlFragment = html.innerHTML;
  // 创建一个新的document,注入html片段
  const newDocument = document.implementation.createHTMLDocument('photo');
  newDocument.documentElement.innerHTML = htmlFragment;
  return newDocument;
}
复制代码

四、

最后以后一步,就是生成svg,然后转换成图片,在页面中显示出来。

// 把svg转换成blob或者base64
const getUrlForSvg = function (svg, useBlob) {
  if (useBlob) {
    return URL.createObjectURL(new Blob([svg], { type: "image/svg+xml" }))
  }
  return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
}

// 绘制图片
const render = function (html) {
  const xhtml = new XMLSerializer().serializeToString(html);
  const str = (
    `
      <svg viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg">
        <foreignObject x="0" y="0" width="600" height="600">
          ${xhtml}
        </foreignObject>
      </svg>
    `
  );
  const src = getUrlForSvg(str);
  const img = document.getElementById('img');
  img.width = 600;
  img.height = 600;
  img.src = src;
};
复制代码

整个运行流程如下

const takePhoto = function () {
  const doc = createDocument();
  const html = doc.getElementsByTagName('html')[0];
  replaceImgSrc(doc)
    .then(_ => {
      return getOuterLink(doc)
    })
    .then(_ => {
      render(html);
    })
    .catch(err => {
      console.log('err', err)
    });
};

btn.onclick = takePhoto;
复制代码

image.png
生成的图片和我们页面中相比,不能说是非常相似,可以说是一模一样。

结语

写到这里已经是凌晨2点多了,边写边看欧洲杯,看到德国被淘汰也是有点郁闷。看英国赢球了跟看雄鹿赢球了是一个感觉,吃了屎一样难受。

言归正传,首先很感谢 玄魂 大佬,这篇文章也是看了大佬的公众号文章,才边摸索,边写出来的。原文链接贴一贴

[mp.weixin.qq.com/s/ugrBaCIWY…]

大家可以关注下大佬的公众号,都是好东西。而且原文中的也比较详细,我这里的代码,没有对css文件做更深层的处理,因为外链的css中也会有@import,也会有外链,我是水平没到,大家看看原文应该能了解得更透彻一点。

其次,写完这篇也挺有成就感的,一年前就欠下的技术债,终于还上了,说明我还是在努力进步的,也不是很混。文章可能还有些瑕疵,我也有些不懂的地方,还要再请教下大佬,如果有问题,后续也会更改,我自己也是一知半解,只能尽量不误人子弟哇。两天混了2篇,准备直接休息一礼拜继续看我的flutter去了。

3点的比赛也没啥好看的,整把炉石,然后迎接新的一天~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享