SVG+Canvas實現生成海報——不失真
持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第8天,點選檢視活動詳情
前沿
最近遇到很多需求裡有分享海報功能,一開始採用 html2Canvas
實現,後來發現 IOS各種相容問題,就改用純 Canvas
繪製的,簡單的還好,但稍微複雜一些的頁面就比較費勁了。
後來想,如果後面遇到非常複雜的頁面需要生成圖片海報,用 Canvas
繪製豈不是很苦逼麼。後來真的現有遇到的問題做了些梳理並查閱了一些資料,開始有了新的思路,ts-dom-to-image就這麼誕生。
遇到的常見問題
- 跨越圖片的問題
看 html2Canvas
生成出來的和原頁面差距很大。
文件裡說跨域圖片需要加給圖片加上 crossorigin="anonymous"
或者呼叫時傳入{allowTaint:true,useCORS:true}
這裡的useCORS
與crossorigin
一樣,原始碼 cache-storage.ts 檔案中的loadImage
方法裡可以看出,感興趣可以去看看,這裡我截了個圖:
但是crossorigin
IOS 相容只支援15.1以上版本。
你以為這樣就解決了麼,雖然針對跨域圖片做了處理, 但是還是有些機型有問題,比如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
,最終輸出你需要的不同格式的圖。可以看看實現的流程圖:
實現
先看看需要實現哪些功能,如: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-font、
process-style、
process-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: http://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
圖片最終在 SVG上呈現的結果是這樣的:
處理字型
這裡說的主要是自定義的字型的處理,其原理是把當前頁面的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 的形式存在,比如這樣:
看看DEMO測試 效果:
Style 處理
把所有元素的樣式轉化成行內樣式一一對應到克隆元素上,樣式處理這塊包括針對background
圖片的處理,上面跨域圖片這塊已經講過,另外針對偽類做了處理,目前只處理了before
、after
。
有個要注意地方,在 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
不支援的,常用的樣式做了測試
下面示例中的都包含這些屬性:
最後就回到一開始的實現上方法,主要就是生成 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
就萬分感謝了!