ichuan.net

自信打不死的心态活到老

js 实现浏览器客户端下载 svg 图像为 png 图片

最近一个项目需要实现一个比较变态的需求:客户要能够把网页里 js 绘制的统计图(amcharts 绘的 svg 图)保存成 png(或 jpg) 图片,方便贴在 word 文档里。

技术上当然是可行的。最复杂的方法是把网页在服务器端用 PhantomJS 跑一遍,截图给用户下载。再简单些,可以把网页上已经生成的 svg 代码 post 到服务器端,同样 PhantomJS 打开,截图。但这样做太麻烦,我想能不能直接在客户端截图让用户下载,于是研究了半天,基本得出一个方法:

  1. XMLSerializersvg 元素转成字符串
  2. data:image/svg+xml 的 data-uri 协议把 svg 显示在一个 img 标签里
  3. 在一个 canvas 元素里用 drawImage() 把含有 svg 图像的 img 绘制出来
  4. canvas.toDataURL() 得到 canvas 的二进制图片数据(base64 编码后)
  5. 再用 data-uri 协议或者 URL.createObjectURL 把得到的图片二进制数据放到一个 a 链接的 href 属性中
  6. 设置 a.download 属性,并触发 a.click(),让浏览器给用户弹出一个下载框

相关代码如下:

var tmpA = document.createElement('A');
tmpA.download = '图片.png';
var svg2png = function (svg) {
  svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  var canvas = document.createElement('canvas'),
      ctx = canvas.getContext('2d'),
      img = new Image,
      svgXml = (new XMLSerializer()).serializeToString(svg);
  img.onload = function () {
    canvas.width = img.width;
    canvas.height = img.height;
    ctx.drawImage(img, 0, 0);
    tmpA.href = canvas.toDataURL('image/png');
    tmpA.click();
  };
  img.src = 'data:image/svg+xml,' + svgXml;
};

看起来很完美,可是执行到 canvas.toDataURL 这一行时浏览器报错:

Uncaught Error: SecurityError: DOM Exception 18

原来在 canvas 里包含的元素是外域时,调用 canvas.toDataURL() 时会由于安全问题被浏览器禁止。chrome 里认为 data-uri 也是独立一个域,所以刚才的 canvas 里画了一个 data-uri 的 img,就没法通过 toDataURL() 得到 svg 转成的图片数据了。

看来这种方法行不通了,很完美的一个方案就差这一步实现不了了。后来再 google 了半天,终于找到一个绕过第三方库 canvg,用它可以绕过上面的限制。还是上面的逻辑,不过绘图由 img 换成了 canvg,代码如下:

var tmpA = document.createElement('A');
tmpA.download = '图片.png';

var svg2png = function (svg) {
  var canvas = document.createElement('canvas'),
      evt = document.createEvent('MouseEvent');

  svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
  evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0,
                      false, false, false, false, 0, null);

  canvg(canvas, (new XMLSerializer()).serializeToString(svg), {
    ignoreMouse: true,
    ignoreAnimation: true,
    renderCallback: function () {
      tmpA.href = canvas.toDataURL('image/png');
      tmpA.dispatchEvent(evt);
    }
  });
};

只要传入一个 svg 字串,canvg 就可以生成一个含有该图像的 canvas 元素。而且因为它是解析 svg 结构和 css 转成 canvas 原生绘图过程的,没有不同域的问题,所以 canvas.toDataURL() 可以正常执行。

调用 svg2png 函数,传入一个 svg 元素,浏览器就会弹出一个下载框,下载后是该 svg 的 png 格式图片。

svgforeignObject 元素可以内嵌 html 内容,是不是意味着可以用 svg 包裹任意 html 内容,然后可以用上述方法下载图片?遗憾的是 canvg 现在并不支持 foreignObject 元素。

可以看看 demo 页面。在该页面里,鼠标移到某个 amchart 画的图上,右上角会有个“下载”链接,点击后会在浏览器客户端生成一个图片下载框。chrome 和 firefox 最新版测试通过。

浏览器处理链接href属性的一个差异

浏览器在处理链接的href属性时,如果该链接是相对路经(如href="/test")或者锚点(如href="#test"),IE下读取到的值是拼接好的完整地址,而其他浏览器下读到的是原始href属性值。

加入有个index.html内容如下:

<html><body>

<a id="b" href="#test"></a>
<script>
    alert(document.getElementById('b').getAttribute('href'));
</script>

</body></html>

分别用Chrome、Firefox、IE打开,可以看到差别:

enter image description here

计划.IN 开源

如同之前承诺的那样,现在将这个项目开源了,欢迎 fork:

https://github.com/ichuan/jihua

jihua.in 这个网站会一直运行,我本身就一直在用。

有个 mm 图在 doc/design.mm,是那晚简单做的一个设计。其他介绍就不写了,都在 github 那里。