前言
svg 是一種矢量圖形,在 web 上應用很廣泛,但是很多時候由于應用的場景,常常需要將 svg 轉為 png 格式,下載到本地等。隨著瀏覽器對 html 5 的支持度越來越高,我們可以把 svg 轉為 png 的工作交給瀏覽器來完成。
一般方式
- 創建 imageimage,src = xxx.svg;
- 創建 canvas,dragImage 將圖片貼到 canvas 上;
- 利用 toDataUrl 函數,將 canvas 的表示為 url;
- new image, src = url, download = download.png;
但是,在轉換的時候有時有時會碰到如下的如下的兩個問題:
問題 1 :瀏覽器對 canvas 限制
Canvas 的 W3C 的標準上沒有提及 canvas 的最大高/寬度和面積,但是每個廠商的瀏覽器出于瀏覽器性能的考慮,在不同的平臺上設置了最大的高/寬度或者是渲染面積,超過了這個閾值渲染的結果會是空白。測試了幾種瀏覽器的 canvas 性能如下:
- chrome (版本 46.0.2490.80 (64-bit))
- 最大面積:268, 435, 456 px^2 = 16, 384 px * 16, 384 px
- 最大寬/高:32, 767 px
- firefox (版本 42.0)
- 最大面積:32, 767 px * 16, 384 px
- 最大寬/高:32, 767px
- safari (版本 9.0.1 (11601.2.7.2))
- 最大面積: 268, 435, 456 px^2 = 16, 384 px * 16, 384 px
- ie 10(版本 10.0.9200.17414)
- 最大寬/高: 8, 192px * 8, 192px
在一般的 web 應用中,可能很少會超過這些限制。但是,如果超過了這些限制,則會導致導出為空白或者由于內存泄露造成瀏覽器崩潰。
而且從另一方面來說,導出 png 也是一項很消耗內存的操作,粗略估算一下,導出 16, 384 px * 16, 384 px 的 svg 會消耗 16384 * 16384 * 4 / 1024 / 1024 = 1024 M 的內存。所以,在接近這些極限值的時候,瀏覽器也會反應變慢,能否導出成功也跟系統的可用內存大小等等都有關系。
對于這個問題,有如下兩種解決方法:
- 將數據發送給后端,在后端完成轉換;
- 前端將 svg 切分成多個圖片導出;
第一種方法可以使用 PhantomJS、inkscape、ImageMagick 等工具,相對來說比較簡單,這里我們主要探討第二種解決方法。
svg 切分成多個圖片導出
思路:瀏覽器雖然對 canvas 有尺寸和面積的限制,但是對于 image 元素并沒有明確的限制,也就是第一步生成的 image 其實顯示是正常的,我們要做的只是在第二步 dragImage 的時候分多次將 image 元素切分并貼到 canvas 上然后下載下來。 同時,應注意到 image 的載入是一個異步的過程。
關鍵代碼:
// 構造 svg Url,此處省略將 svg 經字符過濾后轉為 url 的過程。 var svgUrl = DomURL.createObjectURL(blob); var svgWidth = document.querySelector('#kity_svg').getAttribute('width'); var svgHeight = document.querySelector('#kity_svg').getAttribute('height'); // 分片的寬度和高度,可根據瀏覽器做適配 var w0 = 8192; var h0 = 8192; // 每行和每列能容納的分片數 var M = Math.ceil(svgWidth / w0); var N = Math.ceil(svgHeight / h0); var idx = 0; loadImage(svgUrl).then(function(img) { while(idx < M * N) { // 要分割的面片在 image 上的坐標和尺寸 var targetX = idx % M * w0, targetY = idx / M * h0, targetW = (idx + 1) % M ? w0 : (svgWidth - (M - 1) * w0), targetH = idx >= (N - 1) * M ? (svgHeight - (N - 1) * h0) : h0; var canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'); canvas.width = targetW; canvas.height = targetH; ctx.drawImage(img, targetX, targetY, targetW, targetH, 0, 0, targetW, targetH); console.log('now it is ' + idx); // 準備在前端下載 var a = document.createElement('a'); a.download = 'naotu-' + idx + '.png'; a.href = canvas.toDataURL('image/png'); var clickEvent = new MouseEvent('click', { 'view': window, 'bubbles': true, 'cancelable': false }); a.dispatchEvent(clickEvent); idx++; } }, function(err) { console.log(err); }); // 加載 image function loadImage(url) { return new Promise(function(resolve, reject) { var image = new Image(); image.src = url; image.crossOrigin = 'Anonymous'; image.onload = function() { resolve(this); }; image.onerror = function(err) { reject(err); }; }); }
說明:
- 由于在前端下載有瀏覽器兼容性、用戶體驗等問題,在實際中,可能需要將生成后的數據發送到后端,并作為一個壓縮包下載。
- 分片的尺寸這里使用的是 8192 * 9192,在實際中,為了增強兼容性和體驗,可以根據瀏覽器和平臺做適配,例如在 IOS 下的 safari 的最大面積是 4096 *4096。
問題 2 :導出包含圖片的 svg
在導出的時候,還會碰到另一個問題:如果 svg 里面包含圖片,你會發現通過以上方法導出的 png 里面,原來的圖片是不顯示的。一般認為是 svg 里面包含的圖片跨域了,但是如果你把這個圖片換成本域的圖片,還是會出現這種情況。
圖片中上部分是導出前的%20svg,下圖是導出后的%20png。svg%20中的圖片是本域的,在導出后不顯示。
問題來源
我們按照文章最開始提出的步驟,逐步排查,會發現在第一步的時候,svg%20中的圖片就不顯示了。也就是,當%20image%20元素的%20src%20為一個%20svg,并且%20svg%20里面包含圖片,那么被包含的圖片是不會顯示的,即使這個圖片是本域的。
W3C%20關于這個問題并沒有做說明,最后在%20https://bugzilla.mozilla.org/show_bug.cgi?id=628747%20找到了關于這個問題的說明。意思是:禁止這么做是出于安全考慮,svg%20里面引用的所有 外部資源 包括%20image,%20stylesheet,%20script%20等都會被阻止。
里面還舉了一個例子:假設沒有這個限制,如果一個論壇允許用戶上傳這樣的%20svg%20作為頭像,就有可能出現這樣的場景,一位黑客上傳%20svg%20作為頭像,里面包含代碼:<image%20xlink:href="http://evilhacker.com/myimage.png">(假設這位黑客擁有對于 evilhacker.com 的控制權),那么這位黑客就完全能做到下面的事情:
- 只要有人查看他的資料,evilhacker.com 就會接收到一次 ping 的請求(進而可以拿到查看者的 ip);
- 可以做到對于不同的 ip 地址的人展示不一樣的頭像;
- 可以隨時更換頭像的外觀(而不用通過論壇管理員的審核)。
看到這里,大概就明白了整個問題的來龍去脈了,當然還有一點原因可能是避免圖像遞歸。
解決辦法
思路:由于安全因素,其實第一步的時候,圖片已經顯示不出來了。那么我們現在考慮的方法是在第一步之后遍歷 svg 的結構,將所有的 image 元素的 url、位置和尺寸保存下來。在第三步之后,按順序貼到 canvas 上。這樣,最后導出的 png 圖片就會有 svg 里面的 image。關鍵代碼:
// 此處略去生成 svg url 的過程 var svgUrl = DomURL.createObjectURL(blob); var svgWidth = document.querySelector('#kity_svg').getAttribute('width'); var svgHeight = document.querySelector('#kity_svg').getAttribute('height'); var embededImages = document.querySelectorAll('#kity_svg image'); // 由 nodeList 轉為 array embededImages = Array.prototype.slice.call(embededImages); // 加載底層的圖 loadImage(svgUrl).then(function(img) { var canvas = document.createElement('canvas'), ctx = canvas.getContext("2d"); canvas.width = svgWidth; canvas.height = svgHeight; ctx.drawImage(img, 0, 0); // 遍歷 svg 里面所有的 image 元素 embededImages.reduce(function(sequence, svgImg){ return sequence.then(function() { var url = svgImg.getAttribute('xlink:href') + 'abc', dX = svgImg.getAttribute('x'), dY = svgImg.getAttribute('y'), dWidth = svgImg.getAttribute('width'), dHeight = svgImg.getAttribute('height'); return loadImage(url).then(function(sImg) { ctx.drawImage(sImg, 0, 0, sImg.width, sImg.height, dX, dY, dWidth, dHeight); }, function(err) { console.log(err); }); }, function(err) { console.log(err); }); }, Promise.resolve()).then(function() { // 準備在前端下載 var a = document.createElement("a"); a.download = 'download.png'; a.href = canvas.toDataURL("image/png"); var clickEvent = new MouseEvent("click", { "view": window, "bubbles": true, "cancelable": false }); a.dispatchEvent(clickEvent); }); }, function(err) { console.log(err); }) // 省略了 loadImage 函數 // 代碼和第一個例子相同
說明:
- 例子中 svg 里面的圖像是根節點下面的,因此用于表示位置的 x, y 直接取來即可使用,在實際中,這些位置可能需要跟其他屬性做一些運算之后得出。如果是基于 svg 庫構建的,那么可以直接使用庫里面用于定位的函數,比直接從底層運算更加方便和準確。
- 我們這里討論的是本域的圖片的導出問題,跨域的圖片由于「污染了」畫布,在執行 toDataUrl 函數的時候會報錯。
結語
在這里和大家分享了在前端將 svg 轉為 png 的方法和過程中可能會遇到的兩個問題,一個是瀏覽器對 canvas 的尺寸限制,另一個是導出圖片的問題。當然,這兩個問題還有其他的解決方法,同時由于知識所限,本文內容難免有紕漏,歡迎大家批評指正。
希望本文能幫助到您!
點贊+轉發,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注 {我},享受文章首發體驗!
每周重點攻克一個前端技術難點。更多精彩前端內容私信 我 回復“教程”
原文鏈接:http://fex.baidu.com/blog/2015/11/convert-svg-to-png-at-frontend/
作者:zhangbobell