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))
原则
原则是用来指导实践的。
永远从问题最近的地方开始分析
不要用战术上的勤奋掩饰战略上的懒惰