前端實現 SVG 轉 PNG

發表於2015-11-16

前言

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 的載入是一個非同步的過程。

關鍵程式碼

說明:

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

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

在匯出的時候,還會碰到另一個問題:如果 svg 裡面包含圖片,你會發現通過以上方法匯出的 png 裡面,原來的圖片是不顯示的。一般認為是 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。關鍵程式碼

說明

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

結語

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

相關文章