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; });
åæ³èµ·æ¥ï¼ä¸ºäºèçå 个åè¯ï¼æèè´¹äºå¥½å¤æ¶é´å»è¶è¿ä¸ªåï¼è¿æ¯ä¸å¼å½çãè¿å¶ä¸çé®é¢ä¸ä¹å 为æåè¿å¾å¤å½æ°å¼ä»£ç ï¼æ以å¾åç®æ´ç表达ï¼ä½æ¯æ´å¼å¾è¦éçæ¯ï¼å¨é¢ä¸´ä¸ç¡®å®æ§é®é¢æ¶ææ°çæç»´æ¹å¼ï¼ç¨ä¸å¥å¥è¯è®æ¥èªå·±ââä¸è¦ç¨ææ¯ä¸çå¤å¥æ©é¥°æç¥ä¸çææ°ã
æ们é½ç¥éè¯éªæ¯å¦ä¹ çé«ææ¹å¼ï¼ä½æ¯åä¸å¯ä¹±ç¢°ä¹±æãæå¾é®é¢ä¸ç¿¼èé£ï¼æ们åºå½éµå¾ªç»è¿éªè¯çåååä¸è¦å®³ãä¸å»å¶èï¼åè®°åè®°ã
以ä¸å°±æ¯æ¬æçå¨é¨å容ï¼å¸æ对大家çå¦ä¹ ææ帮å©ï¼ä¹å¸æ大家å¤å¤æ¯æèæ¬ä¹å®¶ã