前端實現 SVG 轉 PNG


 

http://fex.baidu.com/blog/2015/11/convert-svg-to-png-at-frontend/

 

 

 

 

 

前言

svg 是一種矢量圖形,在 web 上應用很廣泛,但是很多時候由於應用的場景,常常需要將 svg 轉為 png 格式,下載到本地等。隨着瀏覽器對 HTML 5 的支持度越來越高,我們可以把 svg 轉為 png 的工作交給瀏覽器來完成。

一般方式

  1. 創建 imageimage,src = xxx.svg;
  2. 創建 canvas,dragImage 將圖片貼到 canvas 上;
  3. 利用 toDataUrl 函數,將 canvas 的表示為 url;
  4. 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 的內存。所以,在接近這些極限值的時候,瀏覽器也會反應變慢,能否導出成功也跟系統的可用內存大小等等都有關系。

對於這個問題,有如下兩種解決方法:

  1. 將數據發送給后端,在后端完成轉換;
  2. 前端將 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); }; }); } 

說明:

  1. 由於在前端下載有瀏覽器兼容性、用戶體驗等問題,在實際中,可能需要將生成后的數據發送到后端,並作為一個壓縮包下載。
  2. 分片的尺寸這里使用的是 8192 * 9192,在實際中,為了增強兼容性和體驗,可以根據瀏覽器和平台做適配,例如在 iOS 下的 safari 的最大面積是 4096 *4096。

問題 2 :導出包含圖片的 svg

在導出的時候,還會碰到另一個問題:如果 svg 里面包含圖片,你會發現通過以上方法導出的 png 里面,原來的圖片是不顯示的。一般認為是 svg 里面包含的圖片跨域了,但是如果你把這個圖片換成本域的圖片,還是會出現這種情況。

導出包含圖片的 svg 示例

圖片中上部分是導出前的 svg,下圖是導出后的 png。svg 中的圖片是本域的,在導出后不顯示。

問題來源

我們按照文章最開始提出的步驟,逐步排查,會發現在第一步的時候,svg 中的圖片就不顯示了。也就是,當 image 元素的 src 為一個 svg,並且 svg 里面包含圖片,那么被包含的圖片是不會顯示的,即使這個圖片是本域的。

W3C 關於這個問題並沒有做說明,最后在 https://bugzilla.mozilla.org/show_bug.cgi?id=628747 找到了關於這個問題的說明。意思是:禁止這么做是出於安全考慮,svg 里面引用的所有 外部資源 包括 image, stylesheet, script 等都會被阻止。

里面還舉了一個例子:假設沒有這個限制,如果一個論壇允許用戶上傳這樣的 svg 作為頭像,就有可能出現這樣的場景,一位黑客上傳 svg 作為頭像,里面包含代碼:<image xlink: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 函數 // 代碼和第一個例子相同 

說明

  1. 例子中 svg 里面的圖像是根節點下面的,因此用於表示位置的 x, y 直接取來即可使用,在實際中,這些位置可能需要跟其他屬性做一些運算之后得出。如果是基於 svg 庫構建的,那么可以直接使用庫里面用於定位的函數,比直接從底層運算更加方便和准確。
  2. 我們這里討論的是本域的圖片的導出問題,跨域的圖片由於「污染了」畫布,在執行 toDataUrl 函數的時候會報錯。

結語

在這里和大家分享了在前端將 svg 轉為 png 的方法和過程中可能會遇到的兩個問題,一個是瀏覽器對 canvas 的尺寸限制,另一個是導出圖片的問題。當然,這兩個問題還有其他的解決方法,同時由於知識所限,本文內容難免有紕漏,歡迎大家批評指正。最后感謝@techird 和 @Naxior 關於這兩個問題的討論。

參考資料

  1. StackOverflow 上關於 canvas 的尺寸限制:http://stackoverflow.com/questions/6081483/maximum-size-of-a-canvas-element
  2. Chromium 關於 canvas 的 issue:https://code.google.com/p/chromium/issues/detail?id=339725
  3. Chrome, Firefox 用到的圖形庫 skia : https://skia.org/
  4. Safari 的關於 canvas 面積限制的源碼:http://trac.webkit.org/browser/trunk/Source/WebCore/html/HTMLCanvasElement.cpp#L67
  5. IE 關於 canvas 的限制說明:https://msdn.microsoft.com/en-us/library/ff975062(v=vs.85).aspx
  6. SVG 加載外部資源的討論:https://bugzilla.mozilla.org/show_bug.cgi?id=628747


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM