CSS、SVG、Canvas对特殊字体的绘制与导出


theme: fancy
highlight: atom-one-dark-reasonable

本文正在参加「金石计划 . 瓜分6万现金大奖」

欢迎关注我的公众号:前端侦探

最近在项目中需要对特殊字体进行绘制与导出,如下

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 去绘制合成

希望这几种方式可以带来一些启发,最后,如果觉得还不错,对你有帮助的话,欢迎点赞、收藏、转发❤❤❤

欢迎关注我的公众号:前端侦探

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容