当前位置: 首页 > 工具软件 > ttf.js > 使用案例 >

ttf文件 python 打开_[译]JS解析TTF字体文件

卢阳成
2023-12-01

把字体拖到下边的方框,获取其中的奥妙!点此获取示例ttf字体文件。

TTF文件拖到这里

在这篇文章,我们计划操作如下:

将字体文件拖入网页,并读取之

尽管ttf文件是为C语言读取设计的,但我们仍试图解析之

读取文件的字形数目,并定位各个字形轮廓的位置

解析每个字形轮廓

最后,把这些字形轮廓呈现到网页上

本文由原始文档从零开始解析ttf文件,并获取字形轮廓坐标。如果需要完整解析ttf文件,并获取字体文件的各个属性,以下第三方库可能是更优选择:

—–译者注

用Javascript读取文件

这… 好像很危险。 不过,放心吧,只有把文件拖动到网页上,才能用javascript读取它。通过处理dragover(拖入方框)和drop(释放鼠标)事件,我们可以读取拖进方框的文件。

在页面接听到drop事件的时候,可以获取该文件的引用(指针),进而读取该文件。这个操作无需与服务器进行交互。 我们还得处理dragover事件,不然它将不能工作。

var dropTarget = document.getElementById("dropTarget");

dropTarget.ondragover = function(e) {

e.preventDefault();

};

dropTarget.ondrop = function(e) {

e.preventDefault();

if (!e.dataTransfer || !e.dataTransfer.files) {

alert("没有读取到文件");

return;

}

var reader = new FileReader();

reader.readAsArrayBuffer(e.dataTransfer.files[0]);

reader.onload = function(e) {

ShowTtfFile(reader.result);

};

};

HTML5文件对象不太方便后续的操作。要想获取文件的原始数据,只能用FileReader异步读取它。我们可以读取为base64编码的字符串或ArrayBuffer。在这里,我们读取ttf文件为ArrayBuffer类型。

解析C结构体

TrueType文件设计的时候,计算机内存还很小。它的设计思路是,先把硬盘上的字体文件拷贝到运行内存,然后在适当的位置读取。字体文件中甚至直接存入了C结构体。要读取TrueType文件,只要把它加载到内存就可以了。我们将做类似的事情。不过,首先需要一些功能函数,以便在文件适当的位置查找并读取各种数据类型。 这个类可以实现以上目的。

function BinaryReader(arrayBuffer)

{

assert(arrayBuffer instanceof ArrayBuffer);

this.pos = 0;

this.data = new Uint8Array(arrayBuffer);

}

BinaryReader.prototype = {

seek: function(pos) {

assert(pos >=0 && pos <= this.data.length);

var oldPos = this.pos;

this.pos = pos;

return oldPos;

},

tell: function() {

return this.pos;

},

getUint8: function() {//读取单字节无符号整型

assert(this.pos < this.data.length);

return this.data[this.pos++];

},

getUint16: function() {//读取双字节无符号整型

return ((this.getUint8() << 8) | this.getUint8()) >>> 0;

},

getUint32: function() {//读取四字节无符号整型

return this.getInt32() >>> 0;

},

getInt16: function() {//读取双字节有符号整型

var result = this.getUint16();

if (result & 0x8000) {

result -= (1 << 16);

}

return result;

},

getInt32: function() {//读取四字节有符号整型

return ((this.getUint8() << 24) |

(this.getUint8() << 16) |

(this.getUint8() << 8) |

(this.getUint8() ));

},

getFword: function() {

return this.getInt16();

},

get2Dot14: function() {//读取定点数,00.00000000000000

return this.getInt16() / (1 << 14);

},

getFixed: function() {//读取定点数,00.00

return this.getInt32() / (1 << 16);

},

getString: function(length) {//由arraybuffer转字符串(ascii编码)

var result = "";

for(var i = 0; i < length; i++) {

result += String.fromCharCode(this.getUint8());

}

return result;

},

getDate: function() {//读取日期

var macTime = this.getUint32() * 0x100000000 + this.getUint32();

var utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);

return new Date(utcTime);

}

};

定点数

除了无符号及有符号8位、16位和32位整型,字体文件中还需要一些其他数据类型。某些特定位数的小数可以用定点数来表示。类似于定点算术,我们只使用二进制而非十进制。假设我们打算写入十进制数字1.53,由于1.53转换成二进制是循环小数,因此不能精确写入文件,不过我们将其改写为153再存入文件。只要把它再除以100,就可以欲获得原始数据1.53。

有关Javascript中的数据类型

Javascript中的数据类型是变化无常的,它通常是32位整型。只要它认为是必要的,就会从有符号类型自动转换为无符号类型。即使不需要,js也可能把数据转换成64位双精度浮点数(double float)。

不过,可以用无符号右移位运算符(>>>)将数据类型强制转换为无符号数。将一个数右移0位,其内部类型就转为无符号整型了。

寻找宝藏

TrueType字体格式的详细说明在苹果公司网站。Truetype文件头是偏移表,记录了其余表在文件中的位置。我们将深入一些表来获取字形轮廓。

每个表有一个校验和,以此保证其正确性。校验和可以通过将该表的所有4字节整数相加模2^32得到。 这段代码用来读取每个表的相对于整个文件的偏移量。

function TrueTypeFont(arrayBuffer)

{

this.file = new BinaryReader(arrayBuffer);

this.tables = this.readOffsetTables(this.file);

this.readHeadTable(this.file);

this.length = this.glyphCount();

}

TrueTypeFont.prototype = {

readOffsetTables: function(file) {

var tables = {};

this.scalarType = file.getUint32();

var numTables = file.getUint16();

this.searchRange = file.getUint16();

this.entrySelector = file.getUint16();

this.rangeShift = file.getUint16();

for( var i = 0 ; i < numTables; i++ ) {

var tag = file.getString(4);

tables[tag] = {

checksum: file.getUint32(),

offset: file.getUint32(),

length: file.getUint32()

};

if (tag !== 'head') {

assert(this.calculateTableChecksum(file, tables[tag].offset,

tables[tag].length) === tables[tag].checksum);

}

}

return tables;

},

calculateTableChecksum: function(file, offset, length)

{

var old = file.seek(offset);

var sum = 0;

var nlongs = ((length + 3) / 4) | 0;

while( nlongs-- ) {

sum = (sum + file.getUint32() & 0xffffffff) >>> 0;

}

file.seek(old);

return sum;

},

好了,现在我们定位了各个表的位置。不过,接下来我们需要读取“head”表。除了记录字体尺寸,更重要的是它定义了字形索引的格式。

readHeadTable: function(file) {

assert("head" in this.tables);

file.seek(this.tables["head"].offset);

this.version = file.getFixed();

this.fontRevision = file.getFixed();

this.checksumAdjustment = file.getUint32();

this.magicNumber = file.getUint32();

assert(this.magicNumber === 0x5f0f3cf5);

this.flags = file.getUint16();

this.unitsPerEm = file.getUint16();

this.created = file.getDate();

this.modified = file.getDate();

this.xMin = file.getFword();

this.yMin = file.getFword();

this.xMax = file.getFword();

this.yMax = file.getFword();

this.macStyle = file.getUint16();

this.lowestRecPPEM = file.getUint16();

this.fontDirectionHint = file.getInt16();

this.indexToLocFormat = file.getInt16();

this.glyphDataFormat = file.getInt16();

},

诸如字形之间的水平距离,建议的最小高度,创建日期等属性,可以从许多表得到。不过我们要专注于埋藏的宝藏 – 字形轮廓。 字形轮廓在“glyf”表中。字形是高度压缩的,每个字形的长度也不同。要快速找到某个字形的位置,我们必须先读取“loca”表—字形索引表。

head表的“indexToLocFormat”值决定了“loca”表是一个2字节还是一个4字节值的数组。如果indexToLocFormat为1,那么loca表每个元素占用4个字节,记录了字形在glyf表的位置序号;否则,loca表每个元素占用2个字节,这个元素乘以2就是是字形在glyf表的位置序号。这样的设计不会导致数据的错乱。

getGlyphOffset: function(index) {

assert("loca" in this.tables);

var table = this.tables["loca"];

var file = this.file;

var offset, old;

if (this.indexToLocFormat === 1) {

old = file.seek(table.offset + index * 4);

offset = file.getUint32();

} else {

old = file.seek(table.offset + index * 2);

offset = file.getUint16() * 2;

}

file.seek(old);

return offset + this.tables["glyf"].offset;

},

现在,给定任何字形的索引,就可以定位该字形的位置。不过接下来,有点小麻烦。

如果两个图形彼此重叠,且路径方向不同(一个逆时针,一个顺时针),那么第二个将切掉第一个形状。字体依照这个约定来从轮廓构建形状。例如,字母O需要有两个轮廓 – 一个用于外圆,一个用于内圆。

不过有两种字形。一种是简单字形,由轮廓构成,如上所述;另一种是复合字形,由简单字形复合而成。要绘制复合字形,我们必须把每个简单字形部件放到到正确的位置。这样,复合字形就能处理带重音的字符(如汉语拼音)。正因为此,字母的重音版本占用的空间非常小。

为了专注于获取字体的精华,我们将暂不考虑复合字形。在这里只是提取那些简单字形。

解析轮廓

此函数将解析字形头,然后调用正确的函数来读取字形。

readGlyph: function(index) {

var offset = this.getGlyphOffset(index);

var file = this.file;

if (offset >= this.tables["glyf"].offset + this.tables["glyf"].length)

{

return null;

}

assert(offset >= this.tables["glyf"].offset);

assert(offset < this.tables["glyf"].offset + this.tables["glyf"].length);

file.seek(offset);

var glyph = {

numberOfContours: file.getInt16(),

xMin: file.getFword(),

yMin: file.getFword(),

xMax: file.getFword(),

yMax: file.getFword()

};

assert(glyph.numberOfContours >= -1);

if (glyph.numberOfContours === -1) {

this.readCompoundGlyph(file, glyph);

} else {

this.readSimpleGlyph(file, glyph);

}

return glyph;

},

简单字形以压缩格式存储。通过使用一系列单字节标识,可以很好地处理重复点以及邻点之间的变动情况。对每一个XY坐标,每个标识字节指示对应点是存储在一个字节还是两个字节中。标志数组之后是X坐标,最后是Y坐标数组。这样设计的好处是,如果X或Y坐标没改变,那么只需要一个字节就可以存储这个点。

我们读取每一个字形,并把这些点拼成(x,y)坐标数组,并记录对渲染非常重要的标识。

readSimpleGlyph: function(file, glyph) {

var ON_CURVE = 1,

X_IS_BYTE = 2,

Y_IS_BYTE = 4,

REPEAT = 8,

X_DELTA = 16,

Y_DELTA = 32;

glyph.type = "simple";

glyph.contourEnds = [];

var points = glyph.points = [];

for( var i = 0; i < glyph.numberOfContours; i++ ) {

glyph.contourEnds.push(file.getUint16());

}

// skip over intructions

file.seek(file.getUint16() + file.tell());

if (glyph.numberOfContours === 0) {

return;

}

var numPoints = Math.max.apply(null, glyph.contourEnds) + 1;

var flags = [];

for( i = 0; i < numPoints; i++ ) {

var flag = file.getUint8();

flags.push(flag);

points.push({

onCurve: (flag & ON_CURVE) > 0

});

if ( flag & REPEAT ) {

var repeatCount = file.getUint8();

assert(repeatCount > 0);

i += repeatCount;

while( repeatCount-- ) {

flags.push(flag);

points.push({

onCurve: (flag & ON_CURVE) > 0

});

}

}

}

function readCoords(name, byteFlag, deltaFlag, min, max) {

var value = 0;

for( var i = 0; i < numPoints; i++ ) {

var flag = flags[i];

if ( flag & byteFlag ) {

if ( flag & deltaFlag ) {

value += file.getUint8();

} else {

value -= file.getUint8();

}

} else if ( ~flag & deltaFlag ) {

value += file.getInt16();

} else {

// value is unchanged.

}

points[i][name] = value;

}

}

readCoords("x", X_IS_BYTE, X_DELTA, glyph.xMin, glyph.xMax);

readCoords("y", Y_IS_BYTE, Y_DELTA, glyph.yMin, glyph.yMax);

}

在网页中绘制字形

最后,我们应该为所有的努力展示一些东西 — 绘制字形。我们可以用HTML5画布API绘制。

这个函数用来控制整个流程。首先,从拖放事件中读取数组,并创建TrueType对象;接下来,删除之前绘制的字形;然后,针对每个字形,创建一个canvas元素并缩放字形,使其高度为字母\’M\’的高度–64像素;最后,由于字体坐标原点在屏幕左下角,但canvas坐标原点在左上角,所以需要垂直翻转一下。

function ShowTtfFile(arrayBuffer)

{

var font = new TrueTypeFont(arrayBuffer);

var width = font.xMax - font.xMin;

var height = font.yMax - font.yMin;

var scale = 64 / font.unitsPerEm;

var container = document.getElementById("font-container");

while(container.firstChild) {

container.removeChild(container.firstChild);

}

for( var i = 0; i < font.length; i++ ) {

var canvas = document.createElement("canvas");

canvas.style.border = "1px solid gray";

canvas.width = width * scale;

canvas.height = height * scale;

var ctx = canvas.getContext("2d");

ctx.scale(scale, -scale);

ctx.translate(-font.xMin, -font.yMin - height);

ctx.fillStyle = "#000000";

ctx.beginPath();

if (font.drawGlyph(i, ctx)) {

ctx.fill();

container.appendChild(canvas);

}

}

}

这里展示了它们是如何绘制的。在此函数中,我们忽略了曲线上的控制点,简单连接了轮廓中的每个点。

drawGlyph: function(index, ctx) {

var glyph = this.readGlyph(index);

if ( glyph === null || glyph.type !== "simple" ) {

return false;

}

var p = 0,

c = 0,

first = 1;

while (p < glyph.points.length) {

var point = glyph.points[p];

if ( first === 1 ) {

ctx.moveTo(point.x, point.y);

first = 0;

} else {

ctx.lineTo(point.x, point.y);

}

if ( p === glyph.contourEnds[c] ) {

c += 1;

first = 1;

}

p += 1;

}

return true;

}

本网站采用BY-NC-SA 4.0协议进行授权 | 转载请注明原文链接:https://www.disidu.com/post/15.html

如果觉得本文对您有帮助或者您心情好~可以微信打赏支持一下本站:↓↓↓

 类似资料: