SVG+Canvas實現生成海報——不失真

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第8天,點擊查看活動詳情

前沿

最近遇到很多需求裏有分享海報功能,一開始採用 html2Canvas 實現,後來發現 IOS各種兼容問題,就改用純 Canvas 繪製的,簡單的還好,但稍微複雜一些的頁面就比較費勁了。

後來想,如果後面遇到非常複雜的頁面需要生成圖片海報,用 Canvas 繪製豈不是很苦逼麼。後來真的現有遇到的問題做了些梳理並查閲了一些資料,開始有了新的思路,ts-dom-to-image就這麼誕生。

遇到的常見問題

  • 跨越圖片的問題

原頁面  

html2Canvas 生成出來的和原頁面差距很大。 文檔裏説跨域圖片需要加給圖片加上 crossorigin="anonymous"或者調用時傳入{allowTaint:true,useCORS:true}這裏的useCORScrossorigin一樣,源碼 cache-storage.ts 文件中的loadImage方法裏可以看出,感興趣可以去看看,這裏我截了個圖:

image.png 但是crossorigin IOS 兼容只支持15.1以上版本。

image.png 你以為這樣就解決了麼,雖然針對跨域圖片做了處理, 但是還是有些機型有問題,比如14.x,13.x等系統還會出現跨域圖片空白問題。

解決方案 : - 模糊失真等問題 - 原因: 手機端屏幕、分辨率和電腦端不同。 > - 因為手機端的屏幕是Retina屏幕。假設圖標icon.png是16x16, 設備是2x的Retina屏,那麼你得準備一個[email protected],分辨率是32x32。iPhone 6 Plus和iPhone 6S Plus是3x的Retina屏,如果要兼容,就要更多的圖。 > - 所謂“Retina”是一種顯示標準,是把更多的像素點壓縮至一塊屏幕裏,從而達到更高的分辨率並提高屏幕顯示的細膩程度。

- 解決方案:使用`SVG`或者調整圖片大小。目前我只想到這兩個方法。我後面方案裏就是採用`SVG`來實現的。
  • CSS屬性支持問題 html2Canvas 官方也有所列舉,比如: box-shadow 、filter、zoom ......

ts-dom-to-image方案的實現

先重點介紹下 SVG,因為方案裏它可是關鍵的一步。最終所有不同類型的圖片輸出都是經過SVG來繪製的。

SVG

SVG是一種矢量格式,除作為照片之外,適用於任何類型的圖像。作為可縮放矢量圖形來説非常實用。這就是設計師更頻繁地使用它的原因。

SVG是一種無損格式 – 意味着它在壓縮時不會丟失任何數據,可以呈現無限數量的顏色,最常用於網絡上的圖形、徽標可供其他高分辨率屏幕上查看。

優點

  • 矢量格式可以隨意調整大小
  • 能夠在代碼或文本編輯器中創建簡單的 SVG 渲染
  • 從 Adobe Illustrator 或 Sketch 設計和導出複雜圖形
  • 可以訪問 SVG 文本
  • SVG 很容易設計風格和腳本
  • 現代瀏覽器支持 SVG 格式,並且面向未來
  • 格式具有高度可壓縮性和輕量級
  • 由於基於文本的格式,因此適合搜索
  • 支持透明度
  • 允許靜止或動畫圖像

缺點

  • 設計 SVG 可能會變得複雜
  • 不建議在某些降級的瀏覽器上呈現
  • 電子郵件客户端支持有限

原理

首先,克隆目標元素已經所有的子類元素,然後對克隆元素做不同資源的處理,比如:字體、圖片、樣式等。 等克隆元素的內容和樣式等都處理完成之後,將其轉換成SVG的Base64數據URL,再生成SVG。最後通過Canvas來繪製生成的SVG,最終輸出你需要的不同格式的圖。可以看看實現的流程圖:

image.png

實現

先看看需要實現哪些功能,如:SVG、JPG、PNG、Blob、PixelData 等類型圖的輸出。這裏實現一個 DomToImage類,類裏實現這些方法即可。 ``` ts export default class DomToImage { public options /* * constructor * @param props 渲染參數 / constructor(options: RenderOptions) { const defaultValue = { quality: 1, // 透明度 cacheBust: false, useCredentials: false, httpTimeout: 30000, scale: window.devicePixelRatio, // 圖片放大倍數 } this.options = { ...defaultValue, ...options } }

toSvg() { // 克隆元素 // 處理克隆元素字體 // 處理圖片和樣式內以及背景圖 // 生成SVG } toPng() { // 具體實現 }

toJpg() { // 具體實現 }

toCanvas() { // 具體實現 }

toBlob() { // 具體實現 }

toPixelData() { // 具體實現 } } `` 這裏主要的是toSVG裏的實現,從流程圖上可以看出,針對克隆元素有process-fontprocess-styleprocess-image`三大處理。這裏就只列舉下關鍵的實現,感興趣的同學可以去看看源碼。

如何解決圖片跨域

這裏我採用XMLHttpRequest+ Blob文件流的方式來處理圖片,包括圖片元素以及樣式內的圖片都會經過這處理。看看具體實現: ``ts export const xhr = (props: { url: string httpTimeout?: number cacheBust?: boolean // 是否繞過緩存 useCredentials?: boolean // 是否跨域 successHandle?: Function | undefined failHandle?: Function | undefined }) => { let { url } = props const { httpTimeout = 30000, cacheBust = false, useCredentials = false, successHandle, failHandle, } = props if (cacheBust) { // Cache bypass so we dont have CORS issues with cached images // Source: https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#Bypassing_the_cache url += (/\?/.test(url) ? '&' : '?') + new Date().getTime() } return new Promise(function (resolve, reject) { const request = new XMLHttpRequest() request.onreadystatechange = handle request.ontimeout = () => reject(timeout of ${httpTimeout}ms occured while fetching resource: ${url}`)

request.responseType = 'blob'
request.timeout = httpTimeout
if (useCredentials) request.withCredentials = true
request.open('GET', url, true)
request.send()
request.onerror = reject;
function handle() {
  if (request.readyState !== 4) return
   if (request.status === 200) {
    if (successHandle instanceof Function) {
      successHandle(request, resolve)
    } else {
      resolve(request)
    }
  } else {
    if (failHandle instanceof Function) {
      failHandle(request, reject)
    } else {
      reject(`cannot fetch resource: ${url}, status: ${request.status}`)
    }
  }
}

}) } 通過這個方法獲取到圖片的`blob`數據流,再通過`fileReader`來讀取並轉換為 Base64 的 Image。 ts export const readUrlFileToBase64 = (props: { url: string httpTimeout?: number cacheBust?: boolean useCredentials?: boolean imagePlaceholder?: string // base64 }): Promise => xhr({ ...props, successHandle: ( request: { response: Blob }, resolve: (arg0: string | ArrayBuffer | null) => void, ) => { const reader = new FileReader() reader.onloadend = function () { const content = reader.result resolve(content) } reader.onerror = (err) => console.error('img url reader fail', err) reader.readAsDataURL(request.response) }, }) ``` 圖片處理的功能實現,接下來就是將圖片元素和樣式背景圖片的圖片都通過這個轉換成 Base64 的形式,最終也內聯形式呈現這克隆元素上。

圖片最終在 SVG上呈現的結果是這樣的: image.png

處理字體

這裏説的主要是自定義的字體的處理,其原理是把當前頁面的document.styleSheets中篩選自定義字體進行處理處理。比如這樣的字體: ```css @font-face { font-family: LiuJianMaoCao-Regular; src: url('LiuJianMaoCao-Regular.ttf'); }

@font-face { font-family: "Al-Black"; font-weight: 1000; src: url("//at.alicdn.com/wf/webfont/QBa4l4xvmwzg/-xLe239h9x-gXbqAqlxsa.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/QBa4l4xvmwzg/pYI44mTrOmZ5yzTP60GEq.woff") format("woff"); font-display: swap; } ``` 這些自定義字體都別轉換成 Base64 的形式存在,比如這樣:

image.png 看看DEMO測試 效果: 4.gif

Style 處理

把所有元素的樣式轉化成行內樣式一一對應到克隆元素上,樣式處理這塊包括針對background圖片的處理,上面跨域圖片這塊已經講過,另外針對偽類做了處理,目前只處理了beforeafter

有個要注意地方,在 safari 上針對 background-size:100% auto; 寫法,正常情況省略auto並沒有問題,但是當有-webkit-background-size時,以它優先垂直方向就會被拉昇,因為在 safari 上通過 getComputedStyle獲取到的樣式會有 -webkit-background-size並沒帶 auto,所以這裏需要特殊處理。具體實現看看下面源碼: ts /** * 設置克隆元素樣式 * @param {CSSStyleDeclaration} sourceNodeCssStyle * @param {CSSStyleDeclaration} cloneNodeCssStyle */ export const setCloneNodeStyleProperty = ( sourceNodeCssStyle: CSSStyleDeclaration, cloneNodeCssStyle: CSSStyleDeclaration, ) => { if (sourceNodeCssStyle.cssText) { cloneNodeCssStyle.cssText = sourceNodeCssStyle.cssText // TODO safari 解析Style兼容問題,100% auto 形式會自動省略 auto,這樣會導致生成的背景圖圖高度會被默認為 100%,結果就是被拉伸了 if ( sourceNodeCssStyle.getPropertyValue('-webkit-background-size') === '100%' && util.checkBrowse().isSafari ) { cloneNodeCssStyle.removeProperty('-webkit-background-size') } } else { for (const key of sourceNodeCssStyle) { if (sourceNodeCssStyle.getPropertyValue(key)) { if (key !== '-webkit-background-size') cloneNodeCssStyle.setProperty( key, sourceNodeCssStyle.getPropertyValue(key), sourceNodeCssStyle.getPropertyPriority(key), ) } } } } 來看看原圖和生成被拉昇的對比結果:

被拉昇了肯定不是我們想要的結果。另外針對 html2Canvas不支持的,常用的樣式做了測試 下面示例中的都包含這些屬性:

1.gif

最後就回到一開始的實現上方法,主要就是生成 Svg 過程實現: ts toSvg() { return Promise.resolve() .then((): any => cloneNode(this.options.targetNode, this.options.filter, true), ) .then(processFonts) .then(checkElementImgToInline) // 圖片和背景圖轉內聯形式 .then(this.applyOptions.bind(this)) .then((clone) => { clone.setAttribute('style', '') return createSvgEncodeUrl( clone, this.options.width || util.width(this.options.targetNode), this.options.height || util.height(this.options.targetNode), ) }) } 其他的類型都是基於 SVG的基礎上再通過 Canvas來繪製的,這一步就相對簡單了,我這裏就不細説了。 另外關於使用以及參數配置可以看下 ts-dom-to-image 説明文檔。

最後

歡迎有興趣的朋友去體驗使用,如有問題歡迎提 issues,也可以在評論去説明。如覺得還不錯的,也不妨點個👍或 start 就萬分感謝了!

資源文獻

crossorigin

html2Canvas

domvas

瀏覽器端網頁截圖方案詳解