注册
web

用了这么多年的字体,你知道它是怎么解析的吗?

大家好呀。


因为之前有过字体解析的相关代码开发,一直想把这个过程记录下来,总觉得这个并不是很难,自认为了解得不算全面,就一拖再拖了。今天仅做简单的抛砖引玉,通过本篇文章,可以知道Opentype.js是如何解析字体的。在遇到对应问题的时候,可以有其它的思路去解决。比如:.ttc的解析。又或者好奇我们开发软件过程中字体是如何解析的。


Opentype.js 使用


看官方readme也可以,这里直接将github代码下载,使用自动化测试目录里的字体文件。


需要注意的是load方法已经被废弃。


function load() {
console.error('DEPRECATED! migrate to: opentype.parse(buffer, opt) See: https://github.com/opentypejs/opentype.js/issues/675');
}

package.json设置为type: module,然后就可以直接使用import了。


import { parse } from './src/opentype.mjs';
import fs from 'fs';
// test/fonts/AbrilFatface-Regular.otf
const buffer = fs.promises.readFile('./test/fonts/AbrilFatface-Regular.otf');
// if not running in async context:
buffer.then(data => {
const font = parse(data);
console.log(font.tables);
})

这样就能得到解析的结果了。


Opentype源码阅读


parseBuffer:解析的入口


通过简单的调用入口,我们可以反查源码。传入文件的ArrayBuffer并返回Font结构的对象,在不清楚会有什么结构的时候,可以通过Font查看,当然了,直接console.log查看更方便。


// Public API ///////////////////////////////////////////////////////////

/**
* Parse the OpenType file data (as an ArrayBuffer) and return a Font object.
* Throws an error if the font could not be parsed.
* @param {ArrayBuffer}
* @param {Object} opt - options for parsing
* @return {opentype.Font}
*/

function parseBuffer(buffer, opt={}) {
// ...
// should be an empty font that we'll fill with our own data.
const font = new Font({empty: true});
}
export {
// ...
parseBuffer as parse,
// ...
};

字体类型判断


接着往下阅读。

根据signature的值,去确认字体类型。粗略看来,这里仅支持了TrueType(.ttf)、CFF(.otf)、WOFFWOFF2


    const signature = parse.getTag(data, 0);
if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') {
} else if (signature === 'OTTO') {
} else if (signature === 'wOFF') {
} else if (signature === 'wOF2') {
} else {
throw new Error('Unsupported OpenType signature ' + signature);
}

还需要注意的是,signature的值是的获取(后续基本都是这样婶儿获取的信息)。从指定偏移位置开始,读取4个字节的数据,并将每个字节转换为字符,最终返回一个4字符的字符串标签。


// Retrieve a 4-character tag from the DataView.
// Tags are used to identify tables.
function getTag(dataView, offset) {
let tag = '';
for (let i = offset; i < offset + 4; i += 1) {
tag += String.fromCharCode(dataView.getInt8(i));
}

return tag;
}

表入口信息获取


再看TrueTypeCFF字体的处理,除了对font.outlinesFormat属性的设置之外。剩余的处理方式都是:获取表的个数numTables,再获取表的入口偏移信息。


numTables = parse.getUShort(data, 4);
tableEntries = parseOpenTypeTableEntries(data, numTables);

// Table Directory Entries //////////////////////////////////////////////
/**
* Parses OpenType table entries.
* @param {DataView}
* @param {Number}
* @return {Object[]}
*/

function parseOpenTypeTableEntries(data, numTables) {
const tableEntries = [];
let p = 12;
for (let i = 0; i < numTables; i += 1) {
const tag = parse.getTag(data, p);
const checksum = parse.getULong(data, p + 4);
const offset = parse.getULong(data, p + 8);
const length = parse.getULong(data, p + 12);
tableEntries.push({tag: tag, checksum: checksum, offset: offset, length: length, compression: false});
p += 16;
}

return tableEntries;
}

function getUShort(dataView, offset) {
return dataView.getUint16(offset, false);
}
// Retrieve an unsigned 32-bit long from the DataView.
// The value is stored in big endian.
function getULong(dataView, offset) {
return dataView.getUint32(offset, false);
}

留意到tableEntries获取的offset是从12开始的,而获取numTables是从4开始的,也仅仅是getUnit16,也就是说4-12中间还会有别的信息。


表信息标准描述


这时候只能通过查看微软排版文档描述,Microsoft Typography documentation: Organization of an OpenType Font
Organization of an Opentype Font.png
按照8bit计算,这些信息之后,刚好是在12个字节开始。


后续的描述就是parseOpenTypeTableEntries的结构信息了。


表入口数据


以选择的AbrilFatface-Regular.otf 为例。我们可以打断点看看,这两步骤得到的结果,这里Opentype提供了网址,就直接在上面断点了。
parse opentype.png
这里有11个表,在入口分别有对应的名称、偏移量、长度、校验和。


表数据解析


有了表入口信息,就可以通过tableEntries获取表的数据了。接下来的代码就是通过对应的tag(name)去选择对应的解析方式。有些表的信息需要依赖于别的表,则先暂时存起来。比如: name表需要依赖language表。


    case 'ltag':
table = uncompressTable(data, tableEntry);
ltagTable = ltag.parse(table.data, table.offset);
break;
// ...
case 'name':
nameTableEntry = tableEntry;
break;
// ...
const nameTable = uncompressTable(data, nameTableEntry);
font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
font.names = font.tables.name;

这里就简单看下ltag表的解析,table = uncompressTable(data, tableEntry);判断是否有压缩,比如WOFF压缩字体,这里没有entry数据就还是原来的。


ltag表的解析


function parseLtagTable(data, start) {
const p = new parse.Parser(data, start);
const tableVersion = p.parseULong();
check.argument(tableVersion === 1, 'Unsupported ltag table version.');
// The 'ltag' specification does not define any flags; skip the field.
p.skip('uLong', 1);
const numTags = p.parseULong();

const tags = [];
for (let i = 0; i < numTags; i++) {
let tag = '';
const offset = start + p.parseUShort();
const length = p.parseUShort();
for (let j = offset; j < offset + length; ++j) {
tag += String.fromCharCode(data.getInt8(j));
}

tags.push(tag);
}

return tags;
}

创了p这个Parser实例,包含各种长度parseShortparseULong等。自动移动offset,避免每次手动传入位置。获取了table的version信息,而后就是循环的获取表内容了。找了好些个字体,都没有ltag表🤦🏻‍♀️


解析小结


这里我们可以初步的了解到整个字体的解析过程,就是按照约定的顺序,有个线头般一点儿一点儿的找到所需,只储存了数据。


如需获取最终字形信息,可能需要经过多个表联合查询,比如loca获取字形数据的偏移量,glyf获取字形数据,又或者camp获取字符代码对应的字形索引。


TTC字体集合的解析


回到前面提出的,ttc字体集合,应该怎么解析呢?参照文档对字体集合的处理 Font Collections,相信大家已经有办法解析了。
TTC header.png
注意:这里截图给出的是1.0的结构,更多的查看文档。


最后


这次的分享就到这里了,对一些有按需解析,自定义解析的场景下,希望对大家有帮助。


作者:斯文的烟鬼去shi吧
来源:juejin.cn/post/7400072326199640100

0 个评论

要回复文章请先登录注册