Angular实现svg和png图片下载实现


Posted in Javascript onMay 05, 2019

我经常思考,在面临一个不确定问题时,以往的经验究竟有无辅助作用?如果把经验遗忘会产生何种程度的影响?在上下求索未果之后,如何找回曾经的感觉,恰若灵光一现?凡此种种,终是要思考总结的,这篇文章便是我的反思之作。

本篇文章会记述一些实用的svg与png之间的转换技巧并强调一种思考原则。

概述

技巧

  1. svg和png图片转换和下载
  2. 解决chrome data url too large下载问题
  3. 解决@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,stackoverflow上给出的绕行方案是用 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图片下载功能的过程中遇到一些坑,这些坑有深有浅,深的直接面向stackoverflow编程绕过,浅的靠个人能力解决。只不过,对解决这些浅坑的过度自信却让我的思维陷入懒惰,导致了长时间的浪费。

这里的浅坑就是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;
 });

回想起来,为了节省几个单词,我耗费了好多时间去趟这个坑,这是不值当的。这其中的问题不乏因为我写过很多函数式代码,所以倾向简洁的表达;但是更值得警醒的是,在面临不确定性问题时懒惰的思维方式,用一句套话训斥自己——不要用战术上的勤奋掩饰战略上的懒惰。

我们都知道试验是学习的高效方式,但是切不可乱碰乱撞、期待问题不翼而飞,我们应当遵循经过验证的原则切中要害、一击制胜,切记切记。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript脚本性能优化注意事项
Nov 18 Javascript
不同浏览器对回车提交表单的处理办法
Feb 13 Javascript
jquery实现的元素的left增加N像素 鼠标移开会慢慢的移动到原来的位置
Mar 21 Javascript
单击复制文字兼容各浏览器的完美解决方案
Jul 04 Javascript
使用indexOf等在JavaScript的数组中进行元素查找和替换
Sep 18 Javascript
jQuery原理系列-常用Dom操作详解
Jun 07 Javascript
原生 JS Ajax,GET和POST 请求实例代码
Jun 08 Javascript
jQuery获取file控件中图片的宽高与大小
Aug 04 Javascript
jQuery3.0中的buildFragment私有函数详解
Aug 16 Javascript
Vue.js动态添加、删除选题的实例代码
Sep 30 Javascript
vue.js语法及常用指令
Oct 29 Javascript
浅谈node模块与npm包管理工具
Jan 03 Javascript
jQuery动态生成的元素绑定事件操作实例分析
May 04 #jQuery
node Buffer缓存区常见操作示例
May 04 #Javascript
JS实现checkbox互斥(单选)功能示例
May 04 #Javascript
jQuery实现条件搜索查询、实时取值及升降序排序的方法分析
May 04 #jQuery
微信小程序实现判断是分享到群还是个人功能示例
May 03 #Javascript
微信小程序基于canvas渐变实现的彩虹效果示例
May 03 #Javascript
微信小程序实现的canvas合成图片功能示例
May 03 #Javascript
You might like
php 图像函数大举例(非原创)
2009/06/20 PHP
记录PHP错误日志 display_errors与log_errors的区别
2012/10/09 PHP
阿里云的WindowsServer2016上部署php+apache
2018/07/17 PHP
jQuery 改变CSS样式基础代码
2010/02/11 Javascript
JavaScript保存并运算页面中数字类型变量的写法
2015/07/06 Javascript
微信企业号开发之微信考勤百度地图定位
2015/09/11 Javascript
一步步教大家编写酷炫的导航栏js+css实现
2016/03/14 Javascript
JavaScript自定义浏览器滚动条兼容IE、 火狐和chrome
2017/01/05 Javascript
jQuery遍历节点方法汇总(推荐)
2017/05/13 jQuery
vue组件实现进度条效果
2018/06/06 Javascript
微信小程序云开发实现数据添加、查询和分页
2019/05/17 Javascript
vue.js iview打包上线后字体图标不显示解决办法
2020/01/20 Javascript
基于小程序请求接口wx.request封装的类axios请求
2020/07/02 Javascript
vuex中store存储store.commit和store.dispatch的用法
2020/07/24 Javascript
编写Python CGI脚本的教程
2015/06/29 Python
python更新列表的方法
2015/07/28 Python
python中计算一个列表中连续相同的元素个数方法
2018/06/29 Python
Django项目中添加ldap登陆认证功能的实现
2019/04/04 Python
详解PANDAS 数据合并与重塑(join/merge篇)
2019/07/09 Python
Python 爬虫实现增加播客访问量的方法实现
2019/10/31 Python
Python图像处理库PIL的ImageFont模块使用介绍
2020/02/26 Python
将 Ubuntu 16 和 18 上的 python 升级到最新 python3.8 的方法教程
2020/03/11 Python
Django中FilePathField字段的用法
2020/05/21 Python
CSS3打造磨砂玻璃背景效果
2016/09/28 HTML / CSS
Uber Eats台湾:寻找附近提供送餐服务的餐厅
2018/05/07 全球购物
正宗的澳大利亚Ugg靴子零售商:UGG Express
2020/04/19 全球购物
大一学生假期实习的自我评价
2013/10/12 职场文书
文言文形式的学生求职信
2013/12/03 职场文书
环境工程专业个人求职信
2013/12/05 职场文书
会计电算化专业毕业生自荐信
2013/12/20 职场文书
市场开发与营销专业求职信
2013/12/31 职场文书
2014年3.15团委活动总结
2014/03/16 职场文书
聘任书的写作格式及范文
2014/03/29 职场文书
工程部经理岗位职责
2015/02/02 职场文书
如何用python绘制雷达图
2021/04/24 Python
apache基于端口创建虚拟主机的示例
2021/04/24 Servers