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

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

本篇文章会记述一些实用的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;
 });

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

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

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

(0)

相关推荐

  • Angular2.0/4.0 使用Echarts图表的示例代码

    前言:在开发中现在需要使用echarts来制作图标,所以就在网络上各种找资料,最后发现ngx-echarts轮子可用.那么就走一波. 方式: 使用echarts和ngx-eachrts两个依赖,借助于ngx..,是因为echarts是基于js编写,没有ts文件.所以就使用ngx-echarts来使用即可. 第一步:安装依赖 npm install echarts --save npm install ngx-echarts --save 第二步:在Module中引入 import { NgxEc

  • Angular2使用SVG自定义图表(条形图、折线图)组件示例

    本文实例讲述了Angular2使用SVG自定义图表(条形图.折线图)组件.分享给大家供大家参考,具体如下: 要求:用户将数据作为参数传进来,通过类型决定渲染何种类型的图标. demo: html: <ngo-chart [inputParams]="options"></ngo-chart> ts: options = { type: 'line', //图表类型 xAxis: { //X轴的数据 data: ['Mon', 'Tue', 'Wed', 'Thu

  • 使用angularjs创建简单表格

    初步接手人生的第一个项目,需要用angularjs制作表格和实现各种功能,因此遇到了各种问题和以前不熟悉的知识点,在此记录下来,以供大家学习交流,解决方式可能并不完善或符合规范,如果大家有更好的方式欢迎指出,由于这个表格功能的制作是一点点添加上去的,因此我也分成几个部分介绍,日后如增加了新的功能也会不时更新的 首先,表格采用的是BootStrap样式编辑的,主要使用的是angularjs,为了方便也有jQuery的方法,在测试时需自行引入bootstrap,angularjs和jq的文件. 正文

  • 详解angularjs实现echart图表效果最简洁教程

    本文介绍了详解angularjs实现echart图表效果最简洁教程,分享给大家,具体如下: ehcart是百度做的数据图表,基于原生js.接口和配置都写的很好很易读,还可以用于商用. 一 echart包引用 下载解压,放入lib中. 下载地址:echart_jb51.rar 并在index.html中引用如图两个js文件. app.js中引用angular-echarts 二 页面 html页面 <!--饼图--> <div> <donut-chart config=&quo

  • 详解AngularJS中的表格使用

    表格数据本质上通常是重复的.ng-repeat指令,可以用来方便地绘制表格.下面的示例说明使用ng-repeat指令来绘制表格. <table> <tr> <th>Name</th> <th>Marks</th> </tr> <tr ng-repeat="subject in student.subjects"> <td>{{ subject.name }}</td>

  • Angular中实现树形结构视图实例代码

    近两年当中使用Angular开发过很多项目,其中也涉及到一些树形结构视图的显示,最近的在项目中封装了大量的组件,一些组件也有树形结构的展示,所以写出来总结一下. 相信大家都知道,树结构最典型的例子就是目录结构了吧,一个目录可以包含很多子目录,子目录又可以包含若干个子孙目录,那咱们今天就以目录结构为例来说明一下Angular中树结构的实现. 首先,我们希望封装一个组件,用于显示整个目录的树形机构,代码如下: <!DOCTYPE html> <html ng-app="treeDe

  • AngularJS实现表格的增删改查(仅限前端)

    用AngularJS实现对表格的增删改查(仅限前端),具体代码: <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>实现表格的增删改查</title> <meta http-equiv="keywords" content="keyword1,keyword2,keywo

  • Angular.js与Bootstrap相结合实现表格分页代码

    先给大家简单介绍angular.js和bootstrap基本概念. AngularJS 是一个 JavaScript 框架.它可通过 <script> 标签添加到 HTML 页面. AngularJS 通过 指令 扩展了 HTML,且通过 表达式 绑定数据到 HTML. Bootstrap,来自 Twitter,是目前最受欢迎的前端框架.Bootstrap 是基于 HTML.CSS.JAVASCRIPT 的,它简洁灵活,使得 Web 开发更加快捷. 最近一直学习Angular.js,在学习过程

  • 最棒的Angular2表格控件

    现在市面上有大量的JavaScript数据表格控件,包括开源的第三方的和自产自销的.可以说Wijmo的Flexgrid是目前适应Angular 2的最好的表格控件. Angular 2数据表格基本要求: 更小.更快.更熟悉. 为了使用Angular 2表格,首先你需要了解表格的基本要求.FlexGrid开始于1996年,当时使用C++为Visual Basic编写的控件.多年来,它不断进化并在多个平台得到完善,尤其是JavaScript平台.FlexGrid 因为Flex的理念而命名,控件应当包

  • angularjs表格ng-table使用备忘录

    项目中用到angularjs的表格ng-table,功能相当强大,像搜索.排序.checkbox.分页.每页表格显示数目等都有.API,demo什么的也只能参考官网了.这里做个备忘,哪天肯定还会用到. HTML: <!DOCTYPE html> <html> <meta charset="utf-8"/> <head> <script data-require="angular.js@*" data-semver

随机推荐