CSS、SVG、Canvas對特殊字型的繪製與匯出

XboxYan 發表於 2022-11-28
CSS
歡迎關注我的公眾號:前端偵探

最近在專案中需要對特殊字型進行繪製與匯出,如下

image-20221126162313642

簡單解釋一下:所謂繪製,就是視覺上可以看到就行(預覽狀態),匯出呢,就是將看到的轉換成圖片(或者Canvas),以便於後續處理。

這裡總結了 3 種方式,分別是 CSS 、 SVG、Canvas,來看看各自有什麼差異和優缺點吧

一、CSS 的繪製與匯出

首先來看 CSS ,這是最簡單的繪製方式了。

假設 HTML是這樣的

<div class="text">前端偵探</div>

加點樣式

.text{
  display: flex;
  width: 200px;
  height: 200px;
  justify-content: center;
  align-items: center;
  background-color: rebeccapurple;
  color: #fff;
  font-size: 36px;
  font-family: MFMengYuan-Regular;
}

這裡給了一個特殊的字型MFMengYuan-Regular(造字工坊夢緣體),當然現在肯定是沒有效果,因為系統並沒有這樣的字型

image-20221126135024684

為了使這個特殊字型生效,需要手動透過@font-face去定義

@font-face {
  font-family: "MFMengYuan-Regular";
  src: url("https://webfontsource.yuewen.com/api/v1/yfont/font.eot?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2"); /* IE9 */
  src: local('☺'),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.woff2?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2") format("woff2"),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.woff?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2") format("woff"),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.ttf?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2");
}

這裡引用的是一個線上生成的字型,對於 CSS 來說也是小菜一碟,效果如下

image-20221126135628477

是不是非常輕鬆?

CSS 繪製非常容易,但現在僅僅是視覺上的,那如何將這個樣式轉換成圖片匯出呢?

在這裡,需要藉助 SVG 中的foreignObject元素,透過這個元素,可以將 HTML嵌入到SVG中,例如

<svg xmlns="http://www.w3.org/2000/svg">
  <foreignObject width="200" height="200">
      <body xmlns="http://www.w3.org/1999/xhtml">
        <div>前端偵探</div>
      </body>
    </foreignObject>
</svg>
一些截圖工具庫,比如 html2canvas都依賴 foreignObject 這個特性

SVG本質上就是圖片,然後就可以將這個圖片繪製到 Canvas 上,進一步進行圖片合成和處理了,整體思路如下:

img

不過需要注意的是,SVG是一個獨立的圖片,必須包含繪製內容的全部資訊,比如這裡需要手動將style樣式內嵌到div中,就像這樣(程式碼結構可能不是很好看)

<div class="text">
    <style>
    @font-face {
      font-family: "MFMengYuan-Regular";
  src: url("https://webfontsource.yuewen.com/api/v1/yfont/font.eot?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2"); /* IE9 */
  src: local('☺'),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.woff2?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2") format("woff2"),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.woff?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2") format("woff"),
    url("https://webfontsource.yuewen.com/api/v1/yfont/font.ttf?base64=0&font=MFMengYuan-Regular&text=%E5%89%8D%E7%AB%AF%E4%BE%A6%E6%8E%A2");
    }
      .text{
        display: flex;
        width: 200px;
        height: 200px;
        justify-content: center;
        align-items: center;
        background-color: rebeccapurple;
        color: #fff;
        font-size: 36px;
        font-family: MFMengYuan-Regular;
      }
    </style>
    前端偵探
  </div>

接下來透過JS將其包裹上foreignObject元素,注意一下特殊字元的轉義

const htmlSvg = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="${width/pixelRatio}" height="${height/pixelRatio}">
      <foreignObject x="0" y="0" width="100%" height="100%">
        <body xmlns="http://www.w3.org/1999/xhtml" style="height:100%;margin:0">
          ${dom.outerHTML}
        </body>
      </foreignObject></svg>`.replace(/"/g,"'").replace(/%/g,"%25").replace(/#/g,"%23").replace(/{/g,"%7B").replace(/}/g,"%7D").replace(/</g,"%3C").replace(/>/g,"%3E");
img.src = htmlSvg

這樣得到的一個SVG字串就是一個完整的圖片了

image-20221126144628187

等等...圖片是出來了,不過字型好像丟失了?🤔

為什麼會這樣呢?原因在於,上面字型使用的是線上字型,線上字型在轉成字元後就是普通的字元了,不會發出請求,自然也不會包含字型的真實資訊了,所以要解決這個問題就必須提前將字型轉成本地base64格式,如下

<div class="text">
    <style>
    @font-face {
      font-family: "MFMengYuan-Regular";
        src: local('☺'),
          url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAwcAA0AAAAAEPgAAAAAAAAAAAAAAAAAA...==) format('woff');
      }
      .text{
        display: flex;
        width: 200px;
        height: 200px;
        justify-content: center;
        align-items: center;
        background-color: rebeccapurple;
        color: #fff;
        font-size: 36px;
        font-family: MFMengYuan-Regular;
      }
    </style>
    前端偵探
  </div>

這樣就正常了(SVG字元可能會比較長)~

image-20221126141254054

同樣也能將這個圖片繪製到Canvas

const context = canvas.getContext('2d');
context.drawImage(htmlSvg, 0, 0, width, height);

效果如下

image-20221126141649061

除此之外,透過Canvas還能將圖片轉成blob地址,相比完整 SVG地址而言,地址更加簡潔,有時候圖片過大,在賦值給圖片src會造成瀏覽器卡頓,儘量用blob方式

canvas.toBlob(function(blob){
  img.src = URL.createObjectURL(blob)
})

效果如下

image-20221126153828546

完整轉換過程可以檢視以下連結:

二、SVG 的繪製與匯出

下面來看SVG方式,相比CSS而言,可能稍微麻煩一點,主要是文字排版方面,同樣需要注意字型base64處理

<svg id="svg" class="text" xmlns='http://www.w3.org/2000/svg' viewBox="0 0 200 200" width="200" height="200">
  <style>
      @font-face {
      font-family: "MFMengYuan-Regular";
        src: local('☺'),
          url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAwcAA0AAAAAEPgAAAAAAAAAAAAAAAAAAAAAAAAAAAA...==) format('woff');
      }
      .text{
        background-color: rebeccapurple;
      }
    </style>
  <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#fff" font-size="36" font-family="MFMengYuan-Regular">前端偵探</text>
</svg>

這裡需要注意一下 SVG 中的文字居中方式,用到了dominant-baseline(基線對齊)和text-anchor(錨點對齊),如下

image-20221127135307866

兩者結合,再配合x=50%y=50%就實現了水平垂直居中效果了,如下

image-20221126143104601

由於已經是SVG了,所以匯出圖片或者繪製到Canvas畫布上就更方便,只需要將整個 dom 結構轉義一下就可以了,無需額外包裹

const htmlSvg = `data:image/svg+xml,${dom.outerHTML}`.replace(/"/g,"'").replace(/%/g,"%25").replace(/#/g,"%23").replace(/{/g,"%7B").replace(/}/g,"%7D").replace(/</g,"%3C").replace(/>/g,"%3E");
img.src = htmlSvg

效果如下

image-20221126143613993

繪製到Canvas上也是同樣的方式

const context = canvas.getContext('2d');
context.drawImage(htmlSvg, 0, 0, width, height);

效果如下

image-20221126141649061

完整轉換過程可以檢視以下連結:

三、Canvas 的繪製與匯出

最後是 Canvas方式。

這裡要繪製的很簡單,就是一個矩形和一行文字,主要步驟如下

const context = canvas.getContext('2d');
context.fillStyle = 'rebeccapurple'// 填充顏色
context.fillRect(0,0,width,height) // 繪製矩形
context.fillStyle = '#fff' // 填充顏色
context.font = `36px MFMengYuan-Regular`; // 設定字型屬性
context.textAlign = 'center';  // 設定文字對齊
context.textBaseline = 'middle'  // 設定基線對齊
context.fillText('前端偵探', width/2, height/2); // 繪製文字

效果如下

image-20221126150622720

不出意料,字型果然沒有繪製,因為系統並沒有這種字型,那如何主動新增字型呢?

這裡有一個策略,Canvas讀取的是頁面上已經渲染過的字型,也就是說頁面上如果提前渲染過該字型,那麼在繪製的時候就可以直接繪製出來,如果字型是動態的,可以透過動態建立

const fontStyle = `
      @font-face {
      font-family: "MFMengYuan-Regular";
        src: local('☺'),
          url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAwcAA0AAAAAEPgAAAAAAAAAAA...==) format('woff');
      }
      `
const style = document.createElement('style')
style.textContent = fontStyle
document.head.appendChild(style)

現在重新繪製,如下

Kapture 2022-11-26 at 15.12.05

可以看到,起初是沒有字型的,重新整理後才繪製新的字型。

原因是,前面這段程式碼僅僅是表示頁面有這個字型,但是並沒有渲染過,透過Canvas繪製後,這個字型才真正被渲染,所以到了第二次字型才生效。

知道原因後,解決就很簡單了。如果不是實時繪製,比如說預覽狀態透過 CSS 繪製,那麼等到 Canvas 繪製的時候(比如透過按鈕點選生成預覽圖),字型當然已經渲染過,自然也不會有這個問題。如果一定要實時繪製,可以採用逐幀比對的方式,一旦影像發生變化,就表示字型渲染成功,實現如下

該方法參考自張鑫旭老師這篇文件:canvas API中文網 - 中文文件 - CanvasRenderingContext2D.font
// 先隨便繪製一個字型
context.font = `36px UNKNOW`;
context.fillText('前端偵探', width/2, height/2);
const dataDefault = context.getImageData(0, 0, width/4, height/2).data;
const detect = function () {
  // 然後繪製實際字型
  context.font = `36px MFMengYuan-Regular`;
    context.fillText('前端偵探', width/2, height/2);
  // 如果前後資料一致,說明字型還沒載入成功,繼續檢測
  var dataNow = context.getImageData(0, 0, width/4, height/2).data;
  if ([].slice.call(dataNow).join('') == [].slice.call(dataDefault).join('')) {
    console.log('沒有變化,重新渲染')
    requestAnimationFrame(detect);
  }
};

這樣就可以實時繪製特殊字型了

image-20221126141649061

Canvas本身就是圖片了,直接可以轉換成圖片或者匯出,這裡就不多介紹了。

完整實現過程可以檢視以下連結:

四、總結一下各自優缺點

下面簡單整理了一下各自實現的難易程度

CSS繪製最簡單,尤其是在文字排版方面,要遠遠領先其他兩種方式

SVG繪製相對比較簡單,在向量圖形處理,比如描邊特效要比 CSS 更有優勢,這兩種方式匯出的難點在於一些外鏈資源的額外處理。

Canvas繪製稍微複雜一些,在特殊字型需要逐幀去檢測是否渲染,優點是繪製出來就是圖片,無需額外匯出

繪製匯出
CSS⭐️⭐️⭐️(簡單)⭐️⭐️(一般)
SVG⭐️⭐️(一般)⭐️⭐️(一般)
Canvas⭐️(複雜)⭐️⭐️⭐️⭐️(超級簡單)

關於 CSS 和 SVG 的選擇可以看實際文字排版需求,比如文字需要換行,字號大小也不一致,像這種情況 CSS 就比較有優勢了,無需去精確計算文字座標

另外,在實際工作中,根據需求可能需要多種方式結合使用,也就是預覽狀態和匯出狀態分別用不同的方式實現,比如圖片混合,在預覽狀態完全可以透過 CSS 實現,在匯出時才透過 Canvas 去繪製合成

希望這幾種方式可以帶來一些啟發,最後,如果覺得還不錯,對你有幫助的話,歡迎點贊、收藏、轉發❤❤❤

歡迎關注我的公眾號:前端偵探