canvg使用小记 2019-1-10 20:04:09

当前有个需求:把svg片段转为png

网上查了下,最后选了canvg这个库。

使用

  1. 引入文件
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdn.jsdelivr.net/npm/canvg/dist/browser/canvg.min.js"></script>

前两个文件我没使用,看起来一个是控制SVG颜色的,一个压根就不知道干什么的,如果你想失焦?

  1. 准备一个canvas元素

不论是页面上已经存在的,还是动态创建的,都ok(但是在后面的一个场景里,你还必须得动态创建)。

  1. 调用转换方法(一共是三种方式)
//load '../path/to/your.svg' in the canvas with id = 'canvas'
canvg('canvas', '../path/to/your.svg')

//load a svg snippet in the canvas with id = 'drawingArea'
canvg(document.getElementById('drawingArea'), '<svg>...</svg>')

//ignore mouse events and animation
canvg('canvas', 'file.svg', { ignoreMouse: true, ignoreAnimation: true })

执行完之后,通过canvasDomObj.toDataURL('image/png')获取图片的base64编码的链接,放到img的src属性里即可。

问题

以上过程在大多数情况下足够用了,但偏偏存在一些奇葩的问题。

外链image

比如我这里需要处理这样的一个片段:

<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="9.324ex" height="2.094ex" viewBox="0 -799.9 4014.7 901.7" role="img" focusable="false" style="vertical-align: -0.236ex;">
  <defs>...</defs>
  <g stroke="currentColor" fill="currentColor" stroke-width="0" transform="matrix(1 0 0 -1 0 0)">
    <!-- 看这里 -->
    <image alt="" title="" y="0" height="598" width="897" transform="translate(0,598.4534362934362) matrix(1 0 0 -1 0 0)" preserveAspectRatio="none" xlink:href="https://www.domain.com/static/demo.svg"></image>
    <!-- 看这里 -->
    <use xlink:href="#E45-MJMATHI-41" x="897" y="0"></use>
    <use xlink:href="#E45-MJMATHI-42" x="1652" y="0"></use>
    <use xlink:href="#E45-MJMATHI-43" x="2416" y="0"></use>
    <use xlink:href="#E45-MJMATHI-44" x="3181" y="0"></use>
  </g>
</svg>

其中存在一个image标签,引了外链https://www.domain.com/static/demo.svg,这就造成了canvg在转换过程中涉及到跨域问题,导致代码报错,没法正常进行下去。虽然canvg配置对象中的useCORS默认为true,却并不妨碍它跨域失败的事实。

解决这个问题的思路就是直接把外链替换成base64的形式。

首先在静态html文档中验证这个思路,对比发现:data:image/png;base64,PD94bWwgdmVyc2lvbj0...这样的编码是不行的,需要data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0...这样的。其中使用到一个在线工具,可以将在线url转换成base64编码的形式:b64.io - image optimisation & base64 encode

搜索js img转base64,网上清一色的答案都是通过canvas绘制img,然后通过canvas.toDataURL()这个方法来得到base64数据。虽说文档中提到toDataURL这个方法接受一个type值来定义编码类型,但是我尝试传入svg+xml并没有什么用。

既然动态生成没戏了,只能定义一个静态变量了(因为我们的image是可枚举的)。也就是说:先把在线url通过工具转换成如data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0...这样的编码。然后定义一个map,这个map是把在线url的末尾name与base64字符串做一个关联。类似一个https://www.domain.com/static/demo.svg这样的在线url可以定义如下对象:

const map = {
  demo: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0...'
}

假如现在有两张图片,外链分别为:https://www.domain.com/static/img1.svghttps://www.domain.com/static/img2.svg

具体解析代码如下:

const BASE64_MAP = {
  img1: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0...',
  img2: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0...'
}
function getImgSrcList (svgObjList) {
  return new Promise((resolve, reject) => {
    let list = []
    Promise.all(Array.from(svgObjList).map((svgObj, index) => {
      return new Promise((innerResolve, innerReject) => {
        // 1. 找到svg片段中所有的image替换
        Array.from(svgObj.querySelectorAll('image')).forEach(img => {
          let href = img.getAttribute('href') || '';
          img.setAttribute('xlink:href', BASE64_MAP[href.slice(href.lastIndexOf('/') + 1, href.lastIndexOf('.'))] || '');
        })
        // 2. 创建独立的canvas容器
        let _area = document.createElement('canvas')
        _area.setAttribute('width', '500px')
        _area.setAttribute('height', '500px')
        _area.setAttribute('style', 'display: none')
        _area.setAttribute('id', 'drawImage' + index)
        document.body.appendChild(_area)
        let area = document.getElementById('drawImage' + index)
        // 3. 转换
        canvg(area, svgObj.outerHTML, {
          renderCallback: function () {
            // 1. 按自己的位置赋值,即index
            list[index] = area.toDataURL('image/png');
            // 2. 清空
            area.outerHTML = '';
            // 3. 结束本次转换
            innerResolve();
          }
        })
      })
    }))
      .then(() => {
        resolve(list);
      })
  })
}

同步与异步

细心的话会留意到,上述调用canvg转换时,传了第三个对象,对象有个renderCallback属性。其他可配置属性参照github主页

大部分情况canvg的转换都是同步的,无需第三个参数。但当涉及到image时(目前我只应用到image这个层面),无论是外链还是base64,canvg的转换都是异步的。

所以要想得到准确的图片,就必须在回调函数中操作。

不支持的元素

实际使用过程中,碰到canvg不识别的元素。

一个是&nbsp;,一个是<line></line>

具体表现是:当svg中包含&nbsp;时,此字符之后的所有内容都变成了空白。当svg中包含<line></line>时,此部分内容转换为空白。

解决第一个&nbsp;的方式就是把svg的所有&nbsp;替换成空格,即

canvg(area, svgObj.outerHTML).replace(/&nbsp;/g, ' ')

第二个问题因为影响不大,暂时忽略。