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 的 trim 函数的代码
Aug 13 Javascript
神奇的代码 通杀各种网站-可随意修改复制页面内容
Jul 17 Javascript
jquery实现点击页面计算点击次数
Jan 23 Javascript
异步安全加载javascript文件的方法
Jul 21 Javascript
JavaScript实现为input与textarea自定义hover,focus效果的方法
Aug 21 Javascript
一步步教大家编写酷炫的导航栏js+css实现
Mar 14 Javascript
JS随机洗牌算法之数组随机排序
Mar 23 Javascript
JS实现获取当前URL和来源URL的方法
Aug 24 Javascript
小发现之浅谈location.search与location.hash的问题
Jun 23 Javascript
React精髓!一篇全概括小结(急速)
May 23 Javascript
JavaScript 浏览器对象模型BOM原理与常见用法实例分析
Dec 16 Javascript
浅谈JavaScript节流和防抖函数
Aug 25 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字符串函数的总结分析
2013/06/05 PHP
php漏洞之跨网站请求伪造与防止伪造方法
2013/08/15 PHP
thinkPHP框架实现类似java过滤器的简单方法示例
2018/09/05 PHP
Laravel使用原生sql语句并调用的方法
2019/10/09 PHP
原生javascript实现图片轮播效果代码
2010/09/03 Javascript
JavaScript高级程序设计 读书笔记之十一 内置对象Global
2012/03/07 Javascript
IE网页js语法错误2行字符1、FF中正常的解决方法
2013/09/09 Javascript
js读取json的两种常用方法示例介绍
2014/10/19 Javascript
JavaScript DOM基础
2015/04/13 Javascript
javascript中replace( )方法的使用
2015/04/24 Javascript
AngularJs篇:使用AngularJs打造一个简易权限系统的实现代码
2016/12/26 Javascript
js实现选项卡内容切换以及折叠和展开效果【推荐】
2017/01/08 Javascript
Thinkphp5微信小程序获取用户信息接口的实例详解
2017/09/26 Javascript
vuex2中使用mapGetters/mapActions报错的解决方法
2018/10/20 Javascript
Layui数据表格判断编辑输入的值,是否为我需要的类型详解
2019/10/26 Javascript
基于form-data请求格式详解
2019/10/29 Javascript
js实现带箭头的进度流程
2020/03/26 Javascript
小程序中使用css var变量(使js可以动态设置css样式属性)
2020/03/31 Javascript
[01:51]历届DOTA2国际邀请赛举办地回顾 TI9落地上海
2018/08/26 DOTA
Python Pandas批量读取csv文件到dataframe的方法
2018/10/08 Python
Python操作配置文件ini的三种方法讲解
2019/02/22 Python
Python学习笔记之读取文件、OS模块、异常处理、with as语法示例
2019/06/04 Python
django-filter和普通查询的例子
2019/08/12 Python
Python配置文件处理的方法教程
2019/08/29 Python
Hotels.com台湾:饭店订房网
2017/09/06 全球购物
Under Armour西班牙官网:美国知名的高端功能性运动品牌
2018/12/12 全球购物
Nike澳大利亚官网:Nike.com (AU)
2019/06/03 全球购物
NHL官方在线商店:Shop.NHL.com
2020/05/01 全球购物
介绍Ibatis的核心类
2013/11/18 面试题
C#中有没有静态构造函数,如果有是做什么用的?
2016/06/04 面试题
北京英语导游词
2015/02/12 职场文书
单位政审意见范文
2015/06/04 职场文书
8个JS的reduce使用实例和reduce操作方式
2021/10/05 Javascript
漫画「处刑少女的生存之道」第3卷封面公开
2022/03/21 日漫
《宝可梦》动画制作25周年到来 官方发布特别纪念视频
2022/04/01 日漫
【海涛DOTA解说】EVE女子战队独家录像加ZSMJ神牛两连发
2022/04/01 DOTA