Angular实现svg和png图片下载实现
æç»å¸¸æèï¼å¨é¢ä¸´ä¸ä¸ªä¸ç¡®å®é®é¢æ¶ï¼ä»¥å¾çç»éªç©¶ç«ææ è¾å©ä½ç¨ï¼å¦ææç»éªéå¿ä¼äº§çä½ç§ç¨åº¦çå½±åï¼å¨ä¸ä¸æ±ç´¢æªæä¹åï¼å¦ä½æ¾åæ¾ç»çæè§ï¼æ°è¥çµåä¸ç°ï¼å¡æ¤ç§ç§ï¼ç»æ¯è¦æèæ»ç»çï¼è¿ç¯æç« ä¾¿æ¯æçåæä¹ä½ã
æ¬ç¯æç« ä¼è®°è¿°ä¸äºå®ç¨çsvgä¸pngä¹é´çè½¬æ¢æå·§å¹¶å¼ºè°ä¸ç§æèååã
æ¦è¿°
æå·§
- svgåpngå¾ç转æ¢åä¸è½½
- è§£å³chrome data url too largeä¸è½½é®é¢
- è§£å³@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; });
åæ³èµ·æ¥ï¼ä¸ºäºèçå 个åè¯ï¼æèè´¹äºå¥½å¤æ¶é´å»è¶è¿ä¸ªåï¼è¿æ¯ä¸å¼å½çãè¿å¶ä¸çé®é¢ä¸ä¹å 为æåè¿å¾å¤å½æ°å¼ä»£ç ï¼æä»¥å¾åç®æ´ç表达ï¼ä½æ¯æ´å¼å¾è¦éçæ¯ï¼å¨é¢ä¸´ä¸ç¡®å®æ§é®é¢æ¶ææ°çæç»´æ¹å¼ï¼ç¨ä¸å¥å¥è¯è®æ¥èªå·±ââä¸è¦ç¨ææ¯ä¸çå¤å¥æ©é¥°æç¥ä¸çææ°ã
æä»¬é½ç¥éè¯éªæ¯å¦ä¹ ç髿æ¹å¼ï¼ä½æ¯åä¸å¯ä¹±ç¢°ä¹±æãæå¾é®é¢ä¸ç¿¼èé£ï¼æä»¬åºå½éµå¾ªç»è¿éªè¯çåååä¸è¦å®³ãä¸å»å¶èï¼åè®°åè®°ã
以ä¸å°±æ¯æ¬æçå¨é¨å容ï¼å¸æå¯¹å¤§å®¶çå¦ä¹ ææå¸®å©ï¼ä¹å¸æå¤§å®¶å¤å¤æ¯æèæ¬ä¹å®¶ã