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 最新版测试通过。

python小工具:tcp proxy和tcp hub

刚才看书,忽然想用 pythonsocket 模块写个 tcp proxy 工具,于是立马动手。写完后运行,改了几个手误,竟再无严重 bug。这点让我很欣慰。

完成的脚本是:tcp_proxy.py。这个脚本作用就是让一台机器变成 tcp 跳板。例如 A 机器想连 B 机器,但不能直接连上;而 C 机器可以直接连 B 机器,也可以与 A 机器连接。则在 C 机器上执行此脚本,A 机器连 C 机器的一个端口,就相当于连到 B 机器的某个端口,间接实现 AB 的通信。这和 HTTP 代理类似。

这个脚本支持多客户端同时连接。

以下是一个在代理机上执行此脚本后,我连接代理机 1234 端口的输出:

$ python tcp_proxy.py -l 1234 -r baidu.com:80 -v
Listening at 0.0.0.0:1234 ...
New clients from 10.2.3.5:56344
10.2.3.5:56344 => 220.181.111.85:80 (481 bytes)
220.181.111.85:80 => 10.2.3.5:56344 (381 bytes)
10.2.3.5:56344 => 220.181.111.85:80 (406 bytes)
New clients from 10.2.3.5:56348
220.181.111.85:80 => 10.2.3.5:56344 (551 bytes)
10.2.3.5:56348 => 123.125.114.144:80 (593 bytes)
123.125.114.144:80 => 10.2.3.5:56348 (197 bytes)
New clients from 10.2.3.5:56352
10.2.3.5:56352 => 220.181.111.86:80 (136 bytes)
220.181.111.86:80 => 10.2.3.5:56352 (381 bytes)
Socket closed by 10.2.3.5:56352
Socket closed by 220.181.111.85:80
Socket closed by 123.125.114.144:80
^CClosing...

上例中 10.2.3.5 相当于 A 机器,而 baidu.com 相当于 B 机器。

测试了代理 ssh 连接,无问题。

还有一种情况是:C 也无法连接 B,但 B 能连接 C。比如 AB 都是处在不同内网的机器,C 是公网上的一台机器。这样就需要 AB 分别连接 CC 然后把两个 socket 中转。

针对这种情况,我改了下原先的脚本,得到一个新的脚本:tcp_hub.py

以下是在公网机器上执行此脚本后的输出。我另外在两个内网机器上分别用 nc 去连公网机器的 1234512346 端口,连接后两个内网机器可以相互 echo 信息:

$ python tcp_hub.py -a 12345 -b 12346 -v
Listening at 0.0.0.0:12345 ...
Listening at 0.0.0.0:12346 ...
New clients from 128.224.233.142:35595 ...
New clients from 128.224.233.142:33668 ...
128.224.233.142:33668 => 128.224.233.142:35595 (2 bytes)
128.224.233.142:35595 => 128.224.233.142:33668 (5 bytes)
Socket closed by 128.224.233.142:33668
Socket closed by 128.224.233.142:35595

使用Endomondo追踪行程

住的地方离公司有一段不近不远的距离,我现在每天都是步行来回。突然对我的步行速度和距离感了兴趣,于是今天起用 Endomondo 来追踪行程。

使用中截图:

enter image description here

最终,从我住处楼下到公司楼下,距离是 2.04 公里,每天走过去花 24 分钟,平均速度 5.11公里/小时。

enter image description here