数字水印的嵌入和提取_无法提取嵌入的字体_pdf无法提取嵌入字体

背景介绍—

Web 项目中,使用一个适合的字体能给用户带来良好的体验。但是字体文件这么多,如果设计师或者开发人员想要查询字体,只能一个个打开,非常影响工作效率。我负责的平台项目刚好需要实现一个功能,能够支持根据固定文字以及用户输入预览字体。在实现这一功能的过程中主要解决两个问题:

现在将问题的解决以及我的思考总结成文。

使用 web 自定义字体—

在聊这两个问题之前,我们先简述怎样使用一个 Web 自定义字体。要想使用一个自定义字体,可以依赖 CSS Fonts Module Level 3 定义的 @font-face 规则。一种基本能够兼容所有浏览器的使用方法如下:

@font-face {    font-family: "webfontFamily"; /* 名字任意取 */    src: url('webfont.eot');         url('web.eot?#iefix') format("embedded-opentype"),         url("webfont.woff2") format("woff2"),         url("webfont.woff") format("woff"),         url("webfont.ttf") format("truetype");    font-style:normal;    font-weight:normal;}.webfont {    font-family: webfontFamily;   /* @font-face里定义的名字 */}

由于 woff2、woff、ttf 格式在大多数浏览器支持已经较好,因此上面的代码也可以写成:

@font-face {    font-family: "webfontFamily"; /* 名字任意取 */    src: url("webfont.woff2") format("woff2"),         url("webfont.woff") format("woff"),         url("webfont.ttf") format("truetype");    font-style:normal;    font-weight:normal;}

有了@font-face 规则,我们只需要将字体源文件上传至 cdn,让 @font-face 规则的 url 值为该字体的地址,最后将这个规则应用在 Web 文字上,就可以实现字体的预览效果。

但这么做我们可以明显发现一个问题,字体体积太大导致的加载时间过长。我们打开浏览器的 Network 面板查看:

可以看到字体的体积为5.5 MB,加载时间为5.13 s。而夸克平台很多的中文字体大小在20~40 MB 之间,可以预想到加载时间会进一步增长。如果用户还处于弱网环境下,这个等待时间是不能接受的。

中文字体体积太大导致加载时间过长—分析原因

那么中文字体相较于英文字体体积为什么这么大,这主要是两个方面的原因:

中文字体包含的字形数量很多,而英文字体仅包含26个字母以及一些其他符号。

中文字形的线条远比英文字形的线条复杂,用于控制中文字形线条的位置点比英文字形更多,因此数据量更大。

我们可以借助于 opentype.js,统计一个中文字体和一个英文字体在字形数量以及字形所占字节数的差异:

字体名称字形数字形所占字节数

FZQingFSJW_Cu.ttf

8731

4762272

JDZhengHT-Bold.ttf

122

18328

夸克平台字体预览需要满足两种方式,一种是固定字符预览, 另一种是根据用户输入的字符进行预览。但无论哪种预览方式,也仅仅会使用到该字体的少量字符,因此全量加载字体是没有必要的,所以我们需要对字体文件做精简。

如何减小字体文件体积unicode-range

unicode-range 属性一般配合 @font-face 规则使用,它用于控制特定字符使用特定字体。但是它并不能减小字体文件的大小,感兴趣的读者可以试试。

fontmin

fontmin 是一个纯 JavaScript 实现的字体子集化方案。前文谈到,中文字体体积相较于英文字体更大的原因是其字形数量更多,那么精简一个字体文件的思路就是将无用的字形移除:

// 伪代码const text = '字体预览'const unicodes = text.split('').map(str => str.charCodeAt(0))const font = loadFont(fontPath)font.glyf = font.glyf.map(g => { // 根据unicodes获取对应的字形})

实际上的精简并没有这么简单,因为一个字体文件由许多表(table)构成,这些表之间是存在关联的,例如 maxp 表记录了字形数量,loca 表中存储了字形位置的偏移量。同时字体文件以 offset table(偏移表) 开头,offset table记录了字体所有表的信息,因此如果我们更改了 glyf 表,就要同时去更新其他表。

在讨论 fontmin 如何进行字体截取之前,我们先来了解一下字体文件的结构:

上面的结构限于字体文件只包含一种字体,且字形轮廓是基于 TrueType 格式(决定 sfntVersion 的取值)的情况,因此偏移表会从字体文件的0字节开始。如果字体文件包含多个字体,则每种字体的偏移表会在 TTCHeader 中指定,这种文件不在文章的讨论范围内。

偏移表(offset table):

TypeNameDescription

uint32

sfntVersion

0x00010000

uint16

numTables

Number of tables

uint16

searchRange

(Maximum power of 2 hmtx -> prep,而表数据没有这个要求。

fontmin 不足之处

fonteditor-core 在截取字体的过程中只会对前文提到的十四张表进行处理,其余表丢弃。每个字体通常还会包含 vhea 和 vmtx 两张表,它们用于控制字体在垂直布局时的间距等信息,如果用 fontmin 进行字体截取后,会丢失这部分信息,可以在文本垂直显示时看出差异(右边为截取后):

fontmin 使用方法

在了解了 fontmin 的原理后,我们就可以愉快的使用它啦。服务器接受到客户端发来的请求后,通过 fontmin 截取字体,fontmin 会返回截取后的字体文件对应的 Buffer,别忘了 @font-face 规则中字体路径是支持 base64 格式的,因此我们只需要将 Buffer 转为 base64 格式嵌入在 @font-face 中返回给客户端,然后客户端将该 @font-face 以 CSS 形式插入 标签中即可。

对于固定的预览内容,我们也可以先生成字体文件保存在 CDN 上,但是这个方式的缺点在于如果 CDN 不稳定就会造成字体加载失败。如果用上面的方法,每一个截取后的字体以 base64 字符串形式存在无法提取嵌入的字体,则可以在服务端做一个缓存,就没有这个问题。利用 fontmin 生成字体子集代码如下:

const Fontmin = require('fontmin')const Promise = require('bluebird')
async function extractFontData (fontPath) { const fontmin = new Fontmin() .src('./font/senty.ttf') .use(Fontmin.glyph({ text: '字体预览' })) .use(Fontmin.ttf2woff2()) .dest('./dist')
await Promise.promisify(fontmin.run, { context: fontmin })()}extractFontData()

对于固定预览内容我们可以预先生成好分割后的字体,对于用户输入的动态预览内容,我们当然也可以按照这个流程:

获取输入 -> 截取字形 -> 上传 CDN -> 生成 @font-face -> 插入页面

按照这个流程来客户端需要请求两次才能获取字体资源(别忘了在 @font-face 插入页面后才会去真正请求字体),并且截取字形和上传 CDN 这两步时间消耗也比较长,有没有更好的办法呢?我们知道字形的轮廓是由一系列位置点确定的,因此我们可以获取 glyf 表中的位置点坐标,通过 SVG 图像将特定字形直接绘制出来。

SVG 是一种强大的图像格式,可以使用 CSS 和 JavaScript 与它们进行交互,在这里主要应用了 path 元素

获取位置信息以及生成 path 标签我们可以借助 opentype.js 完成,客户端得到输入字形的 path 元素后,只需要遍历生成 SVG 标签即可。

减小字体文件体积的优势

下面附上字体截取后文件大小和加载速度对比表格。可以看出,相较于全量加载,对字体进行截取后加载速度快了145 倍。

fontmin 是支持生成 woff2 文件的,但是官方文档并没有更新,最开始我使用的 woff 文件,但是 woff2 格式文件体积更小并且浏览器支持不错

字体名称大小时间

HanyiSentyWoodcut.ttf

48.2MB

17.41s

HanyiSentyWoodcut.woff

21.7KB

0.19s

HanyiSentyWoodcut.woff2

12.2KB

0.12s

字体加载完成前不展示预览内容—

这是在实现预览功能过程中的第二个问题。

在浏览器的字体显示行为中存在阻塞期和交换期两个概念,以 Chrome 为例,在字体加载完成前,会有一段时间显示空白,这段时间被称为阻塞期。如果在阻塞期内仍然没有加载完成,就会先显示后备字体,进入交换期,等待字体加载完成后替换。这就会导致页面字体出现闪烁,与我想要的效果不符。而 font-display 属性控制浏览器的这个行为,是否可以更换 font-display 属性的取值来达到我们的目的呢?

font-display

Block PeriodSwap Period

block

Short

Infinite

swap

None

Infinite

fallback

Extremely Short

Short

optional

Extremely Short

None

字体的显示策略和 font-display 的取值有关,浏览器默认的 font-display 值为 auto,它的行为和取值 block 较为接近。

第一种策略是 FOIT(Flash of Invisible Text),FOIT 是浏览器在加载字体的时候的默认表现形式,其规则如前文所说。

第二种策略是 FOUT(Flash of Unstyled Text),FOUT 会指示浏览器使用后备字体直至自定义字体加载完成,对应的取值为 swap。

两种不同策略的应用:Google Fonts FOIT 汉仪字库 FOUT

在夸克项目中,我希望的效果是字体加载完成前不展示预览内容,FOIT 策略最为接近。但是 FOIT 文本内容不可见的最长时间大约是3s, 如果用户网络状况不太好,那么3s过后还是会先显示后备字体,导致页面字体闪烁,因此 font-display 属性不满足要求。

查阅资料得知,CSS Font Loading API在 JavaScript 层面上也提供了解决方案:

FontFace、FontFaceSet

先看看它们的兼容性:

又是 IE,IE 没有用户不用管

我们可以通过 FontFace 构造函数构造出一个 FontFace 对象:

const fontFace = new FontFace(family, source, descriptors)

source

字体来源,可以是一个 url 或者 ArrayBuffer

descriptors optional

style:font-style

weight:font-weight

stretch:font-stretch

display: font-display (这个值可以设置,但不会生效)

unicodeRange:@font-face 规则的 unicode-ranges

variant:font-variant

featureSettings:font-feature-settings

构造出一个 fontFace 后并不会加载字体,必须执行 fontFace 的 load 方法。load 方法返回一个 promise,promise 的 resolve 值就是加载成功后的字体。但是仅仅加载成功还不会使这个字体生效,还需要将返回的 fontFace 添加到 fontFaceSet。

使用方法如下:

/**  * @param {string} path 字体文件路径  */async function loadFont(path) {  const fontFaceSet = document.fonts  const fontFace = await new FontFace('fontFamily', `url('${path}') format('woff2')`).load()  fontFaceSet.add(fontFace)}

因此,在客户端我们可以先设置文字内容的 CSS 为 opacity: 0,等待 await loadFont(path) 执行完毕后,再将 CSS 设置为 opacity: 1, 这样就可以控制在自定义字体加载未完成前不显示内容。

最后总结—

本文介绍了在开发字体预览功能时遇到的问题和解决方案,限于 OpenType 规范条目很多,在介绍 fontmin 原理部分,仅描述了对 glyf 表的处理,对此感兴趣的读者可进一步学习。

本次工作的回顾和总结过程中,也在思考更好的实现,如果你有建议欢迎和我交流。同时文章的内容是我个人的理解无法提取嵌入的字体,存在错误难以避免,如果发现错误欢迎指正。

感谢阅读!

参考—

最后

无法提取嵌入的字体_pdf无法提取嵌入字体_数字水印的嵌入和提取

点个在看支持我吧

pdf无法提取嵌入字体_数字水印的嵌入和提取_无法提取嵌入的字体

发表回复

后才能评论