Angular实现svg和png图片下载实现
我经常思考,在面临一个不确定问题时,以往的经验究竟有无辅助作用?如果把经验遗忘会产生何种程度的影响?在上下求索未果之后,如何找回曾经的感觉,恰若灵光一现?凡此种种,终是要思考总结的,这篇文章便是我的反思之作。
本篇文章会记述一些实用的svg与png之间的转换技巧并强调一种思考原则。
概述
技巧
- svg和png图片转换和下载
- 解决chrome data url too large下载问题
- 解决@viewchild未及时刷新问题
原则
永远从问题最近的地方开始分析
理解下面这些内容的前提是具备一些angular的编程基础,要求大致处于能自定义component的水平。
假意需求
当我说“假意需求”的时候,其实是将解决方案视作眼下的需求,目的是方便理解。在这个项目中,我们需要把页面上的已经存在的svg元素转换成可下载的svg和png链接。svg是矢量图,适合打印成海报;而png清晰度有限,用作在线预览。
背景知识
下面是svg(scalable vector graphics)和canvas在编程方式、技术原理、使用范围以及转换程度这4个维度上的对比和评估。这些知识是理解实现svg转换为png的基础。
编程方式
svg是矢量图形语言,canvas提供画布标签和绘制api;
svg提供各种图形,滤镜和动画。canvas只有绘制api,相对原始。
技术原理
svg是矢量图,提供了很多图形,还有完整的动画,事件机制,本身可以独立使用;
canvas基于像素,是一种html元素,只能通过脚本绘制。
适用范围
svg被主流浏览器和svg阅读器支持,canvas只有主流浏览器支持;
svg适用于大面积渲染区域的程序和静态文档,如google地图。canvas适合小范围图像密集型场景,如游戏。
转换程度
svg较难以转换成png或者jpeg格式的图片,不过canvas较容易。
技巧
假设主页面 app.component.html 面已经有一个component,它的内容如下:
<app-template #template></app-template>
其中 <app-template></app-template> 是一个自定义的component,它代表了一个svg文件,svg的内容存放在 template.component.html 中,而 template.component.ts 的定义如下:
// template.component.ts @component({ selector: 'app-template', templateurl: './template.component.html', styleurls: ['./template.component.scss'], }) export class templatecomponent implements oninit { ngoninit() { } }
当然,这个template.component需要在 app.module.ts 中声明后才能在 app.component.html 中使用。
注意, #template 是angular5之后引入的语法,它的全称是 template reference variable (#var) ,功能在于引用其所指向的dom元素。
接下来要解决的就是如何在component中引用页面上的svg元素并将它转化成png格式的图片。
svg和png图片转换和下载
1. 获取元素
angular中提供一种叫做 viewchild 的注解,可以帮助我们引用到页面中的svg元素,此处就是 #template .
@component({ selector: 'app-root', templateurl: './app.component.html', styleurls: ['./app.component.scss'], }) export class appcomponent implements ondestroy { @viewchild('template') template: { svgref: elementref }; ngondestroy(): void { } }
获取svg元素的方式为 this.template.svgref.nativeelement .
2. 图片转换
有了svg元素,接下来需要考虑的是如何对其编程。svg和html在浏览器的内存中都是以dom树的形式存在,所以想要对svg进行编程,就得利用svg的dom interface. 比如说我们要获取 <svg> 元素中的各项属性,就需要使用 svgsvgelement编程接口 。
svg转换成png并不直接,但是我们知道canvas转换成png非常简单。所以有种思路是将svg转换成canvas再转成png. canvas有个 drawimage 函数,可以将图片绘制到画布上,该函数的输入源是 htmlimageelement 或者另外的canvas元素。
也就是说,如果我们能把svg转换成 htmlimageelement 即 <img> ,那么上述过程就顺理成章连成一串了。
第一步是将svg元素转换成dataurl.
private tosvgdataurl(viewersvg: svgsvgelement): string { const svg = viewersvg.clonenode(true) as svgsvgelement; svg.setattribute('width', '600px'); const base64data = btoa(unescape(encodeuricomponent(svg.outerhtml))); return `data:image/svg+xml;base64,${base64data}`; }
第二步是将dataurl转换成 <img> .
function loadimage(url: string): observable<htmlimageelement> { const result = new subject<htmlimageelement>(); const image = document.createelement('img'); image.src = url; image.addeventlistener('load', () => { result.next(image); }); return result.asobservable(); }
第三步是将 <img> 转换成canvas.
private topngdataurl(img: htmlimageelement): string { const canvas = document.createelement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getcontext('2d').drawimage(img, 0, 0); return canvas.todataurl('image/png'); }
canvas转成png图片就是上述一句 todataurl 的调用。
3. 图片下载
上面的三个步骤可以合起来。
private generatedownloadurl() { const svgdataurl = this.tosvgdataurl(this.template.svgref.nativeelement); loadimage(svgdataurl) .pipe(map(this.topngdataurl)) .subscribe(url => { this.pngurl = url; this.svgurl = svgdataurl; }); }
<a> 元素的 href 属性是可以接受dataurl的,所以我们把svg dataurl和png dataurl赋值给成员变量pngurl与svgurl即可,最后标注download属性表示这是一条下载链接。
<a [href]="svgurl" target="_blank" download="template.svg">下载 svg 版本</a> <a [href]="pngurl" target="_blank" download="template.png">下载 png 版本</a>
解决chrome data url too large下载问题
上述过程看上去顺利流畅,但是事实上一旦图片过大,在下载时,chrome浏览器会抛出网络错误。这是chrome/chormium内核存在已久的bug,*上给出的绕行方案是用 url.createobjecturl(blob) 取而代之。
private tosvg(viewersvg: svgsvgelement): string { const svg = viewersvg.clonenode(true) as svgsvgelement; svg.setattribute('width', '600px'); const blob = new blob([svg.outerhtml], {type: 'image/svg+xml'}); const url = url.createobjecturl(blob); return url; }
对于png的处理也可以很灵活。
private topng(img: htmlimageelement): observable<string> { const canvas = document.createelement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getcontext('2d').drawimage(img, 0, 0); const result = new subject<string>(); canvas.toblob(blob => { const url = url.createobjecturl(blob); result.next(url); }); return result.asobservable(); }
不过,因为浏览器的安全警告,url需要经过sanitize才能放行。这在angular里可以导入domsanitizer处理。
import {domsanitizer, saferesourceurl} from '@angular/platform-browser'; ... constructor(private sanitizer: domsanitizer) { }
原来的代码得返回saferesourceurl.
private tosvg(viewersvg: svgsvgelement): saferesourceurl { const svg = viewersvg.clonenode(true) as svgsvgelement; svg.setattribute('width', '600px'); const blob = new blob([svg.outerhtml], {type: 'image/svg+xml'}); const url = url.createobjecturl(blob); const safeurl = this.sanitizer.bypasssecuritytrustresourceurl(url); return safeurl; }
private topng(img: htmlimageelement): observable<saferesourceurl> { const canvas = document.createelement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getcontext('2d').drawimage(img, 0, 0); const result = new subject<saferesourceurl>(); canvas.toblob(blob => { const url = this.sanitizer.bypasssecuritytrustresourceurl(url.createobjecturl(blob)); result.next(url); }); return result.asobservable(); }
原来的合并操作相应修改。
private generatedownloadurl() { this.svgurl = this.tosvg(this.template.svgref.nativeelement); const svgdataurl = this.tosvgdataurl(this.template.svgref.nativeelement); loadimage(svgdataurl) .pipe(flatmap(this.topng)) // 此处有坑 .subscribe(url => { this.pngurl = url; }); }
值得注意的是原来的pipe map 改成了 flatmap ,因为 topng 返回还是一个observable,而不是简单的值。
这样看上去是没有问题的,但是如上面这段代码的注释: 此处有坑 。坑在哪里?稍后我会在原则处作深入探讨,现在暂且搁置,进入下一个技术话题。
解决@viewchild未及时刷新问题
@viewchild取得页面元素可能不是最新的,angular的change detection需要时间完成刷新,所以有很短时间的延迟。这对于我的程序而言是不能容忍的。延迟虽不能容忍,但是等待刷新之后再处理图片还是可以的,所以解决方案就是等待一秒钟再做图片转换。
private waitforviewchildready() { return new promise<string>((resolve) => { const wait = settimeout(() => { cleartimeout(wait); resolve('workaround!'); }, 1000); }); }
终章程序调用如下。
this.waitforviewchildready() .then(this.generatedownloadurl()) .catch(err => console.error(err))
原则
原则是用来指导实践的。
永远从问题最近的地方开始分析
不要用战术上的勤奋掩饰战略上的懒惰
我个人对angular并不十分熟悉,在实现svg和png图片下载功能的过程中遇到一些坑,这些坑有深有浅,深的直接面向*编程绕过,浅的靠个人能力解决。只不过,对解决这些浅坑的过度自信却让我的思维陷入懒惰,导致了长时间的浪费。
这里的浅坑就是javascript臭名昭著的this scope问题。
回顾一下上面有坑的代码,
loadimage(svgdataurl) .pipe(flatmap(this.topng)) // 此处有坑 .subscribe(url => { this.pngurl = url; });
topng 的代码如下,
private topng(img: htmlimageelement): observable<saferesourceurl> { const canvas = document.createelement('canvas'); canvas.width = img.width; canvas.height = img.height; canvas.getcontext('2d').drawimage(img, 0, 0); const result = new subject<saferesourceurl>(); canvas.toblob(blob => { const url = this.sanitizer.bypasssecuritytrustresourceurl(url.createobjecturl(blob)); result.next(url); }); return result.asobservable(); }
程序运行时,抛出了一个错误 cannot read bypasssecuritytrustresourceurl of undefined.
第一反应是我是不是写错了变量名,再三验证之后发现没有写错。然而这一步其实完全没必要,原因在于这些变量都是编辑器辅助补全的。
紧接着,我在 toblob 方法插入了 console.log(this.sanitizer) ,运行后打印的结果是 undefined 。这能说明什么?程序执行到这里了?其实这种做法也没必要,因为控制台的错误信息明确表明这段代码执行到了,并且出错了。
然后,我开始思考“难道我写的angular的注入方式不对?”,在遍寻angular的官方文档和样例之后,我确信注入方式没有问题。这步有可取之处,因为对angular本身不够熟悉,查文档是合理的行为,但是解决思路离目标太远,程序的问题应该通过debug解决。
无奈之下,我开始怀疑包依赖下载出现问题,所以用了最愚蠢的方法,删除 node_modules ,然后重新下载全部依赖。这是一步耗时的操作,最大的浪费就发生在这里。我把原来对于探索问题总结的基本原则 分析得从最近的路开始 忘得一干二净。尝试无果之后,我没有从牛角尖中跳出来,遗忘了 花时间放空自己 原则,还是持续纠结,直至最后放弃。
第二天早上,喝了杯咖啡,脑袋清醒了些。在 topng 方法外,我插入 console.log(this.sanitizer) ,发现这个对象完好地出现在命令行中,此刻突然灵感一现,回忆起几年前写过一篇关于javascript作用域的文章,可不就是this指针的问题么?
loadimage(svgdataurl) .pipe(flatmap(this.topng.bind(this))) // 注意此处bind(this) .subscribe(url => { this.pngurl = url; });
所以用 bind(this) 锁定this的指向,然后发现程序运行正常,一切就都豁然开朗了。值得一提的是,这只是最便宜的修复,其实更可取的做法是写全函数体。
loadimage(svgdataurl) .pipe(flatmap(img => this.topng(img))) // 注意此处完整函数体 .subscribe(url => { this.pngurl = url; });
回想起来,为了节省几个单词,我耗费了好多时间去趟这个坑,这是不值当的。这其中的问题不乏因为我写过很多函数式代码,所以倾向简洁的表达;但是更值得警醒的是,在面临不确定性问题时懒惰的思维方式,用一句套话训斥自己——不要用战术上的勤奋掩饰战略上的懒惰。
我们都知道试验是学习的高效方式,但是切不可乱碰乱撞、期待问题不翼而飞,我们应当遵循经过验证的原则切中要害、一击制胜,切记切记。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
推荐阅读
-
php 实现svg转化png格式的方法分析
-
Python实现批量把SVG格式转成png、pdf格式的代码分享
-
Angular ui-roter 和AngularJS 通过 ocLazyLoad 实现动态(懒)加载模块和依赖
-
基于jquery和svg实现超炫酷的动画特效
-
Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)
-
详解angular2实现ng2-router 路由和嵌套路由
-
Angular实现点击按钮控制隐藏和显示功能示例
-
利用angular、react和vue实现相同的面试题组件
-
利用SVG和CSS3来实现一个炫酷的边框动画
-
Angular4学习笔记之实现绑定和分包