Angular实现svg和png图片下载实现(2)

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))

原则

原则是用来指导实践的。

永远从问题最近的地方开始分析

不要用战术上的勤奋掩饰战略上的懒惰

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/20c74ff6e7737e77a9e39b9d613078d1.html