http://fex.baidu.com/blog/2015/11/convert-svg-to-png-at-frontend/

前言

svg 是一种矢量图形,在 web 上应用很广泛,但是很多时候由于应用的场景,常常需要将 svg 转为 png 格式,下载到本地等。随着浏览器对 HTML 5 的支持度越来越高,我们可以把 svg 转为 png 的工作交给浏览器来完成。

一般方式

  1. 创建 imageimage,src = xxx.svg;
  2. 创建 canvas,dragImage 将图片贴到 canvas 上;
  3. 利用 toDataUrl 函数,将 canvas 的表示为 url;
  4. new image, src = url, download = download.png;

但是,在转换的时候有时有时会碰到如下的如下的两个问题:

问题 1 :浏览器对 canvas 限制

Canvas 的 W3C 的标准上没有提及 canvas 的最大高/宽度和面积,但是每个厂商的浏览器出于浏览器性能的考虑,在不同的平台上设置了最大的高/宽度或者是渲染面积,超过了这个阈值渲染的结果会是空白。测试了几种浏览器的 canvas 性能如下:

  • chrome (版本 46.0.2490.80 (64-bit))

    • 最大面积:268, 435, 456 px^2 = 16, 384 px * 16, 384 px
    • 最大宽/高:32, 767 px
  • firefox (版本 42.0)

    • 最大面积:32, 767 px * 16, 384 px
    • 最大宽/高:32, 767px
  • safari (版本 9.0.1 (11601.2.7.2))

    • 最大面积: 268, 435, 456 px^2 = 16, 384 px * 16, 384 px
  • ie 10(版本 10.0.9200.17414)

    • 最大宽/高: 8, 192px * 8, 192px

在一般的 web 应用中,可能很少会超过这些限制。但是,如果超过了这些限制,则会导致导出为空白或者由于内存泄露造成浏览器崩溃。

而且从另一方面来说,导出 png 也是一项很消耗内存的操作,粗略估算一下,导出 16, 384 px * 16, 384 px 的 svg 会消耗 16384 * 16384 * 4 / 1024 / 1024 = 1024 M 的内存。所以,在接近这些极限值的时候,浏览器也会反应变慢,能否导出成功也跟系统的可用内存大小等等都有关系。

对于这个问题,有如下两种解决方法:

  1. 将数据发送给后端,在后端完成转换;
  2. 前端将 svg 切分成多个图片导出;

第一种方法可以使用 PhantomJS、inkscape、ImageMagick 等工具,相对来说比较简单,这里我们主要探讨第二种解决方法。

svg 切分成多个图片导出

思路:浏览器虽然对 canvas 有尺寸和面积的限制,但是对于 image 元素并没有明确的限制,也就是第一步生成的 image 其实显示是正常的,我们要做的只是在第二步 dragImage 的时候分多次将 image 元素切分并贴到 canvas 上然后下载下来。 同时,应注意到 image 的载入是一个异步的过程。

关键代码

  1. // 构造 svg Url,此处省略将 svg 经字符过滤后转为 url 的过程。
  2. var svgUrl = DomURL.createObjectURL(blob);
  3. var svgWidth = document.querySelector('#kity_svg').getAttribute('width');
  4. var svgHeight = document.querySelector('#kity_svg').getAttribute('height');
  5. // 分片的宽度和高度,可根据浏览器做适配
  6. var w0 = 8192;
  7. var h0 = 8192;
  8. // 每行和每列能容纳的分片数
  9. var M = Math.ceil(svgWidth / w0);
  10. var N = Math.ceil(svgHeight / h0);
  11. var idx = 0;
  12. loadImage(svgUrl).then(function(img) {
  13. while(idx < M * N) {
  14. // 要分割的面片在 image 上的坐标和尺寸
  15. var targetX = idx % M * w0,
  16. targetY = idx / M * h0,
  17. targetW = (idx + 1) % M ? w0 : (svgWidth - (M - 1) * w0),
  18. targetH = idx >= (N - 1) * M ? (svgHeight - (N - 1) * h0) : h0;
  19. var canvas = document.createElement('canvas'),
  20. ctx = canvas.getContext('2d');
  21. canvas.width = targetW;
  22. canvas.height = targetH;
  23. ctx.drawImage(img, targetX, targetY, targetW, targetH, 0, 0, targetW, targetH);
  24. console.log('now it is ' + idx);
  25. // 准备在前端下载
  26. var a = document.createElement('a');
  27. a.download = 'naotu-' + idx + '.png';
  28. a.href = canvas.toDataURL('image/png');
  29. var clickEvent = new MouseEvent('click', {
  30. 'view': window,
  31. 'bubbles': true,
  32. 'cancelable': false
  33. });
  34. a.dispatchEvent(clickEvent);
  35. idx++;
  36. }
  37. }, function(err) {
  38. console.log(err);
  39. });
  40. // 加载 image
  41. function loadImage(url) {
  42. return new Promise(function(resolve, reject) {
  43. var image = new Image();
  44. image.src = url;
  45. image.crossOrigin = 'Anonymous';
  46. image.onload = function() {
  47. resolve(this);
  48. };
  49. image.onerror = function(err) {
  50. reject(err);
  51. };
  52. });
  53. }

说明:

  1. 由于在前端下载有浏览器兼容性、用户体验等问题,在实际中,可能需要将生成后的数据发送到后端,并作为一个压缩包下载。
  2. 分片的尺寸这里使用的是 8192 * 9192,在实际中,为了增强兼容性和体验,可以根据浏览器和平台做适配,例如在 iOS 下的 safari 的最大面积是 4096 *4096。

问题 2 :导出包含图片的 svg

在导出的时候,还会碰到另一个问题:如果 svg 里面包含图片,你会发现通过以上方法导出的 png 里面,原来的图片是不显示的。一般认为是 svg 里面包含的图片跨域了,但是如果你把这个图片换成本域的图片,还是会出现这种情况。

图片中上部分是导出前的 svg,下图是导出后的 png。svg 中的图片是本域的,在导出后不显示。

问题来源

我们按照文章最开始提出的步骤,逐步排查,会发现在第一步的时候,svg 中的图片就不显示了。也就是,当 image 元素的 src 为一个 svg,并且 svg 里面包含图片,那么被包含的图片是不会显示的,即使这个图片是本域的。

W3C 关于这个问题并没有做说明,最后在 https://bugzilla.mozilla.org/show_bug.cgi?id=628747 找到了关于这个问题的说明。意思是:禁止这么做是出于安全考虑,svg 里面引用的所有 外部资源 包括 image, stylesheet, script 等都会被阻止。

里面还举了一个例子:假设没有这个限制,如果一个论坛允许用户上传这样的 svg 作为头像,就有可能出现这样的场景,一位黑客上传 svg 作为头像,里面包含代码:<image xlink:href="http://evilhacker.com/myimage.png">(假设这位黑客拥有对于 evilhacker.com 的控制权),那么这位黑客就完全能做到下面的事情:

  • 只要有人查看他的资料,evilhacker.com 就会接收到一次 ping 的请求(进而可以拿到查看者的 ip);
  • 可以做到对于不同的 ip 地址的人展示不一样的头像;
  • 可以随时更换头像的外观(而不用通过论坛管理员的审核)。

看到这里,大概就明白了整个问题的来龙去脉了,当然还有一点原因可能是避免图像递归。

解决办法

思路:由于安全因素,其实第一步的时候,图片已经显示不出来了。那么我们现在考虑的方法是在第一步之后遍历 svg 的结构,将所有的 image 元素的 url、位置和尺寸保存下来。在第三步之后,按顺序贴到 canvas 上。这样,最后导出的 png 图片就会有 svg 里面的 image。关键代码

  1. // 此处略去生成 svg url 的过程
  2. var svgUrl = DomURL.createObjectURL(blob);
  3. var svgWidth = document.querySelector('#kity_svg').getAttribute('width');
  4. var svgHeight = document.querySelector('#kity_svg').getAttribute('height');
  5. var embededImages = document.querySelectorAll('#kity_svg image');
  6. // 由 nodeList 转为 array
  7. embededImages = Array.prototype.slice.call(embededImages);
  8. // 加载底层的图
  9. loadImage(svgUrl).then(function(img) {
  10. var canvas = document.createElement('canvas'),
  11. ctx = canvas.getContext("2d");
  12. canvas.width = svgWidth;
  13. canvas.height = svgHeight;
  14. ctx.drawImage(img, 0, 0);
  15. // 遍历 svg 里面所有的 image 元素
  16. embededImages.reduce(function(sequence, svgImg){
  17. return sequence.then(function() {
  18. var url = svgImg.getAttribute('xlink:href') + 'abc',
  19. dX = svgImg.getAttribute('x'),
  20. dY = svgImg.getAttribute('y'),
  21. dWidth = svgImg.getAttribute('width'),
  22. dHeight = svgImg.getAttribute('height');
  23. return loadImage(url).then(function(sImg) {
  24. ctx.drawImage(sImg, 0, 0, sImg.width, sImg.height, dX, dY, dWidth, dHeight);
  25. }, function(err) {
  26. console.log(err);
  27. });
  28. }, function(err) {
  29. console.log(err);
  30. });
  31. }, Promise.resolve()).then(function() {
  32. // 准备在前端下载
  33. var a = document.createElement("a");
  34. a.download = 'download.png';
  35. a.href = canvas.toDataURL("image/png");
  36. var clickEvent = new MouseEvent("click", {
  37. "view": window,
  38. "bubbles": true,
  39. "cancelable": false
  40. });
  41. a.dispatchEvent(clickEvent);
  42. });
  43. }, function(err) {
  44. console.log(err);
  45. })
  46. // 省略了 loadImage 函数
  47. // 代码和第一个例子相同

说明

  1. 例子中 svg 里面的图像是根节点下面的,因此用于表示位置的 x, y 直接取来即可使用,在实际中,这些位置可能需要跟其他属性做一些运算之后得出。如果是基于 svg 库构建的,那么可以直接使用库里面用于定位的函数,比直接从底层运算更加方便和准确。
  2. 我们这里讨论的是本域的图片的导出问题,跨域的图片由于「污染了」画布,在执行 toDataUrl 函数的时候会报错。

结语

在这里和大家分享了在前端将 svg 转为 png 的方法和过程中可能会遇到的两个问题,一个是浏览器对 canvas 的尺寸限制,另一个是导出图片的问题。当然,这两个问题还有其他的解决方法,同时由于知识所限,本文内容难免有纰漏,欢迎大家批评指正。最后感谢@techird 和 @Naxior 关于这两个问题的讨论。

参考资料

  1. StackOverflow 上关于 canvas 的尺寸限制:http://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element
  2. Chromium 关于 canvas 的 issue:https://code.google.com/p/chromium/issues/detail?id=339725
  3. Chrome, Firefox 用到的图形库 skia : https://skia.org/
  4. Safari 的关于 canvas 面积限制的源码:http://trac.webkit.org/browser/trunk/Source/WebCore/html/HTMLCanvasElement.cpp#L67
  5. IE 关于 canvas 的限制说明:https://msdn.microsoft.com/en-us/library/ff975062(v=vs.85).aspx
  6. SVG 加载外部资源的讨论:https://bugzilla.mozilla.org/show_bug.cgi?id=628747

前端实现 SVG 转 PNG的更多相关文章

  1. 前端技术-svg简介与snap.svg.js开源项目的使用

    前言-为什么学习snap.svg.js 前阵子webAPP的技术群里有人感觉到svg+animate的形式感觉很炫,矢量图任意放大且不需要下载图片,并且在手机端效果流畅. (矢量图与位图最大的区别是, ...

  2. 前端图形:SVG与Canvas

    00.前端图形 前端代码实现图形的几种方式:CSS.SVG.Canvas(主要是JavaScript编码) CSS也是可以画图的,需要借助于高宽.边框border.clip-path裁剪.渐变色背景等 ...

  3. 在web前端使用SVG

    前言: 花了些时间了解了一下svg,然而仍然不怎么了解... 第一步:直接在html代码中使用svg. 首先了解几个标签: <svg version="1.1" xmlns= ...

  4. avalon.js实践 svg地图配置工具

    MVVM模式,在很多复杂交互逻辑下面,有很大的优势.现在相关的框架也很多,现在项目中使用了avalon.js,选择它的原因,是兼容性的考虑,当然也要支持下国内开发大牛,至于性能方面的,没有实际测试过, ...

  5. [Ext JS 4]后台自动产生图档

    前言 [Ext JS 4] 实战之将chart导出为png, jpg 格式的文件 承接上一篇, 我们可以做到在Browser端打开一个Chart,并导出为png或是jpg 等格式的图档. 但实际的需求 ...

  6. 如何使用SVG及其动画技术为你的 Web 前端开发带来一些新鲜的体验

    任何有开发经验的前端工程师都会考虑到不成体系的设备生态所带来的挑战.设备间不同的屏幕尺寸.分辨率和比例使得产品难以提供一致的体验,对于那些对产品有着像素级完美追求的人这种体验差异尤其显著! SVG(可 ...

  7. 【SVG】为了前端页面的美丽,我选择学习SVG

    [SVG]为了前端页面的美丽,我选择学习SVG 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 说明 SVG在之前自学的过程中, ...

  8. JavaScript + SVG实现Web前端WorkFlow工作流DAG有向无环图

    一.效果图展示及说明 (图一) (图二) 附注说明: 1. 图例都是DAG有向无环图的展现效果.两张图的区别为第二张图包含了多个分段关系.放置展示图片效果主要是为了说明该例子支持多段关系的展现(当前也 ...

  9. 【27前端】base标签带有href属性会让chrome里的svg元素url失效

    一个chrome的问题,但具体原因不明. 触发条件:chrome浏览器base标签里href属性有值的时候 触发问题:svg里面的元素如果有用url的滤镜和模糊,则会失效,在firefox里和IE10 ...

随机推荐

  1. 越狱Season 1-Episode 5: English, Fitz or Percy

    Season 1, Episode 5: English, Fitz or Percy -Pope: I assume this is about your transfer request for ...

  2. (转)8 Tactics to Combat Imbalanced Classes in Your Machine Learning Dataset

    8 Tactics to Combat Imbalanced Classes in Your Machine Learning Dataset by Jason Brownlee on August ...

  3. feature visualization from ipython notebook

    Feature visualization from ipython notebook Wang Xiao 1. install anaconda2 from: https://www.continu ...

  4. caffe: compile error : undefined reference to `cv::imread(cv::String const&, int)' et al.

    when I compile caffe file : .build_debug/lib/libcaffe.so: undefined reference to `cv::imread(cv::Str ...

  5. The differences between Java EE components and "standard" Java classes

    https://docs.oracle.com/javaee/7/tutorial/overview003.htm ava EE components are written in the Java ...

  6. Tkinter

    单个选项 from Tkinter import * root = Tk() v = IntVar() Radiobutton(root, text="One", variable ...

  7. ASP.NET MVC中的拦截器

    在ASP.NET MVC中,有三种拦截器:Action拦截器.Result拦截器和Exception拦截器, 所谓的拦截器也没有什么的,只是写一个类,继承另一个类和一个接口,顺便实现接口里面的方法而以 ...

  8. JavaScript中“javascript:void(0) ”是什么意思

    来源: <a href="javascript:test();void(0);">here</a> 此处:Javascript中void是一个操作符,该操作 ...

  9. HBase(七): HBase体系结构剖析(下)

    目录: write Compaction splite read Write: 当客户端发起一个Put请求时,首先根据RowKey寻址,从hbase:meta表中查出该Put数据最终需要去的HRegi ...

  10. 【linux】linux服务管理

    ps:xinetd只要求了解