Skip to content

Instantly share code, notes, and snippets.

@smhanov
Last active March 30, 2025 18:19
Show Gist options
  • Save smhanov/f009a02c00eb27d99479a1e37c1b3354 to your computer and use it in GitHub Desktop.
Save smhanov/f009a02c00eb27d99479a1e37c1b3354 to your computer and use it in GitHub Desktop.
Here is my implementation of a TrueType font reader in Typescript. You can read a font directly from an ArrayBuffer, and then call drawText() to draw it. See my article http://stevehanov.ca/blog/index.php?id=143. The second file, OpenType.ts is the same thing but it handles more TrueType files. It is also more coplex
// By Steve Hanov
// [email protected]
// Released to the public domain on April 18, 2020
//import { log } from "./log"
// Usage:
/*
declare let arrayBuffer:ArrayBuffer;
let font = new TrueTypeFont(arrayBuffer);
// Draw 15 pixel high "hello world" at x, y on canvas context ctx
font.drawText(ctx, "hello world", x, y, 15);
*/
// This is a stand-in for my own logging framework. Replace with yours.
let log = {
create(prefix: string) {
return (...args:any[]) => {
console.log(...args);
}
}
};
function assert(condition: boolean, message = "Assertion failed.") {
if (!condition) {
throw new Error(message);
}
}
class BinaryReader {
private pos = 0;
getUint8: () => number;
readonly length: number;
constructor(dataIn: string | ArrayBuffer) {
if (typeof dataIn === "string") {
this.length = dataIn.length;
this.getUint8 = () => {
assert(this.pos < dataIn.length);
return dataIn.charCodeAt(this.pos++) & 0xff;
};
} else {
let data = new Uint8Array(dataIn);
this.length = data.length;
this.getUint8 = () => {
assert(this.pos < data.length);
return data[this.pos++];
}
}
}
log = log.create("BinaryReader");
seek(pos: number) {
assert(pos >= 0 && pos <= this.length);
var oldPos = this.pos;
this.pos = pos;
return oldPos;
}
tell() {
return this.pos;
}
getUint16() {
return ((this.getUint8() << 8) | this.getUint8()) >>> 0;
}
getUint32() {
return this.getInt32() >>> 0;
}
getInt16() {
var result = this.getUint16();
if (result & 0x8000) {
result -= (1 << 16);
}
return result;
}
getInt32() {
return ((this.getUint8() << 24) |
(this.getUint8() << 16) |
(this.getUint8() << 8) |
(this.getUint8()));
}
getFword() {
return this.getInt16();
}
getUFword() {
return this.getUint16();
}
get2Dot14() {
return this.getInt16() / (1 << 14);
}
getFixed() {
return this.getInt32() / (1 << 16);
}
getString(length: number) {
var result = "";
for (var i = 0; i < length; i++) {
result += String.fromCharCode(this.getUint8());
}
return result;
}
getUnicodeString(length: number) {
var result = "";
for (var i = 0; i < length; i += 2) {
result += String.fromCharCode(this.getUint16());
}
return result;
}
getDate() {
var macTime = this.getUint32() * 0x100000000 + this.getUint32();
var utcTime = macTime * 1000 + Date.UTC(1904, 1, 1);
return new Date(utcTime);
}
}
interface CMap {
readonly format: number;
map(charCode: number): number;
}
/**
Cmap format 0 is just a direct mapping from one byte to the glyph index.
*/
class TrueTypeCmap0 implements CMap {
format = 0;
array: number[] = [];
constructor(file: BinaryReader, length: number) {
for (var i = 0; i < 256; i++) {
var glyphIndex = file.getUint8();
this.log(" Glyph[%s] = %s", i, glyphIndex);
this.array.push(glyphIndex);
}
}
log = log.create("CMAP0");
map(charCode: number) {
if (charCode >= 0 && charCode <= 255) {
//this.log("charCode %s maps to %s", charCode, this.array[charCode]);
return this.array[charCode];
}
return 0;
}
}
interface Segment {
idRangeOffset: number;
startCode: number;
endCode: number;
idDelta: number;
}
/**
Cmap format 4 is a list of segments which can possibly include gaps
*/
class TrueTypeCmap4 implements CMap {
format = 4;
cache: { [key: number]: number } = {};
segments: Segment[];
constructor(private file: BinaryReader, length: number) {
var i, segments: Segment[] = [];
// 2x segcount
var segCount = file.getUint16() / 2;
// 2 * (2**floor(log2(segCount)))
var searchRange = file.getUint16();
// log2(searchRange)
var entrySelector = file.getUint16();
// (2*segCount) - searchRange
var rangeShift = file.getUint16();
// Ending character code for each segment, last is 0xffff
for (i = 0; i < segCount; i++) {
segments.push({
idRangeOffset: 0,
startCode: 0,
endCode: file.getUint16(),
idDelta: 0
});
}
// reservePAd
file.getUint16();
// starting character code for each segment
for (i = 0; i < segCount; i++) {
segments[i].startCode = file.getUint16();
}
// Delta for all character codes in segment
for (i = 0; i < segCount; i++) {
segments[i].idDelta = file.getUint16();
}
// offset in bytes to glyph indexArray, or 0
for (i = 0; i < segCount; i++) {
var ro = file.getUint16();
if (ro) {
segments[i].idRangeOffset = file.tell() - 2 + ro;
} else {
segments[i].idRangeOffset = 0;
}
}
/*
for(i = 0; i < segCount; i++) {
var seg = segments[i];
this.log("segment[%s] = %s %s %s %s", i,
seg.startCode, seg.endCode, seg.idDelta, seg.idRangeOffset);
}
*/
this.segments = segments;
}
log = log.create("CMAP4");
map(charCode: number) {
if (!(charCode in this.cache)) {
for (var j = 0; j < this.segments.length; j++) {
var segment = this.segments[j];
if (segment.startCode <= charCode && segment.endCode >=
charCode) {
var index, glyphIndexAddress;
if (segment.idRangeOffset) {
glyphIndexAddress = segment.idRangeOffset + 2 *
(charCode - segment.startCode);
this.file.seek(glyphIndexAddress);
index = this.file.getUint16();
} else {
index = (segment.idDelta + charCode) & 0xffff;
}
this.log("Charcode %s is between %s and %s; maps to %s (%s) roffset=%s",
charCode, segment.startCode, segment.endCode,
glyphIndexAddress, index, segment.idRangeOffset);
this.cache[charCode] = index;
break;
}
}
if (j === this.segments.length) {
this.cache[charCode] = 0;
}
}
return this.cache[charCode];
}
};
class Kern0Table {
swap: boolean;
offset: number;
nPairs: number;
map: { [key: number]: number } = {};
oldIndex = -1;
constructor(
private file: BinaryReader,
vertical: boolean,
cross: boolean) {
this.swap = vertical && !cross || !vertical && cross;
this.file = file;
this.offset = file.tell();
this.nPairs = file.getUint16();
file.getUint16(); // searchRange
file.getUint16(); // entrySelector
file.getUint16(); // rangeShift
for (var i = 0; i < this.nPairs; i++) {
var left = file.getUint16();
var right = file.getUint16();
var value = file.getFword();
this.map[(left << 16) | right] = value;
//this.log("Kern %s/%s->%s", left, right, value);
}
this.reset();
}
log = log.create("KERN0");
reset() {
this.oldIndex = -1;
}
get(glyphIndex: number) {
var x = 0;
if (this.oldIndex >= 0) {
var ch = (this.oldIndex << 16) | glyphIndex;
if (ch in this.map) {
x = this.map[ch];
}
//this.log("Lookup kern pair %s/%s -> %s (%s)",
// this.oldIndex, glyphIndex, x, ch);
}
this.oldIndex = glyphIndex;
if (this.swap) {
return {
x: 0,
y: x
};
} else {
return {
x: x,
y: 0
};
}
}
};
interface Table {
checksum: number;
offset: number;
length: number;
}
interface Point {
x: number;
y: number;
onCurve: boolean;
}
class GylphComponent {
points: Point[] = [];
}
interface Glyph {
contourEnds: number[];
numberOfContours: number;
points: Point[];
xMin: number;
xMax: number;
yMin: number;
yMax: number;
}
export class TrueTypeFont {
private file: BinaryReader;
private cmaps: CMap[] = [];
private kern: Kern0Table[] = [];
private tables: { [key: string]: Table };
private length: number;
private scalarType = 0;
private searchRange = 0;
private entrySelector = 0;
private rangeShift = 0;
private version = 0;
private fontRevision = 0;
private checksumAdjustment = 0;
private magicNumber = 0;
private flags = 0;
private unitsPerEm = 0;
private created = new Date();
private modified = new Date();
private xMin = 0;
private yMin = 0;
private xMax = 0;
private yMax = 0;
private macStyle = 0;
private lowestRecPPEM = 0;
private fontDirectionHint = 0;
private indexToLocFormat = 0;
private glyphDataFormat = 0;
public fullName = "";
public fontFamily = "";
private fontSubFamily = "";
public postscriptName = "";
public ascent = 0;
public descent = 0;
public lineGap = 0;
public advanceWidthMax = 0;
public minLeftSideBearing = 0;
public minRightSideBearing = 0;
public xMaxExtent = 0;
public caretSlopeRise = 0;
public caretSlopeRun = 0;
public caretOffset = 0;
public metricDataFormat = 0;
public numOfLongHorMetrics = 0;
constructor(data: ArrayBuffer | string) {
this.file = new BinaryReader(data);
this.tables = this.readOffsetTables(this.file);
this.readHeadTable(this.file);
this.readNameTable(this.file);
this.readCmapTable(this.file);
this.readHheaTable(this.file);
this.readKernTable(this.file);
this.length = this.glyphCount();
}
log = log.create("TrueType");
readOffsetTables(file: BinaryReader) {
/**
Mandatory tables:
- cmap
- glyf
- head
- hhead
- hmtx
- loca
- maxp
- name
- post
*/
var tables: { [key: string]: Table } = {};
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') {
this.log("Table %s has checksum 0x%s", tag,
tables[tag].checksum.toString(16));
//assert(this.calculateTableChecksum(file, tables[tag].offset,
// tables[tag].length) === tables[tag].checksum);
}
}
return tables;
}
calculateTableChecksum(file: BinaryReader, offset: number,
length: number) {
var old = file.seek(offset);
var sum = 0;
var nlongs = ((length + 3) / 4) >>> 0;
this.log("nlongs=%s length=%s", nlongs, length);
while (nlongs--) {
sum = (sum + file.getUint32()) >>> 0;
}
file.seek(old);
this.log("Checksum calculated is 0x%s", sum.toString(16));
return sum;
}
readHeadTable(file: BinaryReader) {
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();
}
readCmapTable(file: BinaryReader) {
assert("cmap" in this.tables);
var tableOffset = this.tables["cmap"].offset;
file.seek(tableOffset);
var version = file.getUint16(); // must be 0
var numberSubtables = file.getUint16();
// tables must be sorted by platform id and then platform specific
// encoding.
for (var i = 0; i < numberSubtables; i++) {
// platforms are:
// 0 - Unicode -- use specific id 6 for full coverage. 0/4 common.
// 1 - MAcintosh (Discouraged)
// 2 - reserved
// 3 - Microsoft
var platformID = file.getUint16();
var platformSpecificID = file.getUint16();
var offset = file.getUint32();
this.log("CMap platformid=%s specificid=%s offset=%s", platformID,
platformSpecificID, offset);
if (platformID === 3 && (platformSpecificID <= 1)) {
this.readCmap(file, tableOffset + offset);
}
}
// use format 0 table preferably.
//this.cmaps.sort(function(a, b) {
// return a.format - b.format;
//});
}
readCmap(file: BinaryReader, offset: number) {
var oldPos = file.seek(offset);
var format = file.getUint16();
var length = file.getUint16();
var language = file.getUint16();
var cmap;
this.log(" Cmap format %s length %s", format, length);
if (format === 0) {
cmap = new TrueTypeCmap0(file, length);
} else if (format === 4) {
cmap = new TrueTypeCmap4(file, length);
}
if (cmap) {
this.cmaps.push(cmap);
}
file.seek(oldPos);
}
readKernTable(file: BinaryReader) {
if (!("kern" in this.tables)) {
return;
}
var tableOffset = this.tables["kern"].offset;
file.seek(tableOffset);
var version = file.getUint16(); // version 0
var nTables = file.getUint16();
this.log("Kern Table version: %s", version);
this.log("Kern nTables: %s", nTables);
for (var i = 0; i < nTables; i++) {
version = file.getUint16(); // subtable version
var length = file.getUint16();
var coverage = file.getUint16();
var format = coverage >> 8;
var cross = coverage & 4;
var vertical = (coverage & 0x1) === 0;
this.log("Kerning subtable version %s format %s length %s coverage: %s",
version, format, length, coverage);
var kern = null;
if (format === 0) {
kern = new Kern0Table(file, vertical, cross != 0);
} else {
this.log("Unknown format -- skip");
file.seek(file.tell() + length);
}
if (kern) {
this.kern.push(kern);
}
}
}
readNameTable(file: BinaryReader) {
assert("name" in this.tables);
var tableOffset = this.tables["name"].offset;
file.seek(tableOffset);
var format = file.getUint16(); // must be 0
var count = file.getUint16();
var stringOffset = file.getUint16();
for (var i = 0; i < count; i++) {
var platformID = file.getUint16();
var platformSpecificID = file.getUint16();
var languageID = file.getUint16();
var nameID = file.getUint16();
var length = file.getUint16();
var offset = file.getUint16();
var old = file.seek(tableOffset + stringOffset + offset);
var name;
if (platformID === 0 || platformID === 3) {
name = file.getUnicodeString(length);
} else {
name = file.getString(length);
}
this.log("Name %s/%s id %s language %s: %s",
platformID, platformSpecificID, nameID, languageID, name);
file.seek(old);
switch (nameID) {
case 1:
this.fontFamily = name;
break;
case 2:
this.fontSubFamily = name;
break;
case 4:
this.fullName = name;
break;
case 6:
this.postscriptName = name;
break;
}
}
}
readHheaTable(file: BinaryReader) {
assert("hhea" in this.tables);
var tableOffset = this.tables["hhea"].offset;
file.seek(tableOffset);
var version = file.getFixed(); // 0x00010000
this.ascent = file.getFword();
this.descent = file.getFword();
this.lineGap = file.getFword();
this.advanceWidthMax = file.getUFword();
this.minLeftSideBearing = file.getFword();
this.minRightSideBearing = file.getFword();
this.xMaxExtent = file.getFword();
this.caretSlopeRise = file.getInt16();
this.caretSlopeRun = file.getInt16();
this.caretOffset = file.getFword();
file.getInt16(); // reserved
file.getInt16(); // reserved
file.getInt16(); // reserved
file.getInt16(); // reserved
this.metricDataFormat = file.getInt16();
this.numOfLongHorMetrics = file.getUint16();
}
getHorizontalMetrics(glyphIndex: number) {
assert("hmtx" in this.tables);
var file = this.file;
var old = file.seek(this.tables["hmtx"].offset + 4);
var offset = this.tables["hmtx"].offset;
let advanceWidth, leftSideBearing;
if (glyphIndex < this.numOfLongHorMetrics) {
offset += glyphIndex * 4;
old = this.file.seek(offset);
advanceWidth = file.getUint16();
leftSideBearing = file.getInt16();
} else {
// read the last entry of the hMetrics array
old = file.seek(offset + (this.numOfLongHorMetrics - 1) * 4);
advanceWidth = file.getUint16();
file.seek(offset + this.numOfLongHorMetrics * 4 +
2 * (glyphIndex - this.numOfLongHorMetrics));
leftSideBearing = file.getFword();
}
this.file.seek(old);
return {
advanceWidth: advanceWidth,
leftSideBearing: leftSideBearing
};
}
glyphCount() {
assert("maxp" in this.tables);
var old = this.file.seek(this.tables["maxp"].offset + 4);
var count = this.file.getUint16();
this.file.seek(old);
return count;
}
getGlyphOffset(index: number) {
assert("loca" in this.tables);
var table = this.tables["loca"];
var file = this.file;
var offset, old, next;
if (this.indexToLocFormat === 1) {
old = file.seek(table.offset + index * 4);
offset = file.getUint32();
next = file.getUint32();
} else {
old = file.seek(table.offset + index * 2);
offset = file.getUint16() * 2;
next = file.getUint16() * 2;
}
file.seek(old);
if (offset === next) {
// indicates glyph has no outline( eg space)
return 0;
}
//this.log("Offset for glyph index %s is %s", index, offset);
return offset + this.tables["glyf"].offset;
}
readGlyph(index: number): Glyph | null {
var offset = this.getGlyphOffset(index);
var file = this.file;
if (offset === 0 ||
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 = {
contourEnds: [],
numberOfContours: file.getInt16(),
points: [],
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;
}
readSimpleGlyph(file: BinaryReader, glyph: Glyph) {
var ON_CURVE = 1,
X_IS_BYTE = 2,
Y_IS_BYTE = 4,
REPEAT = 8,
X_DELTA = 16,
Y_DELTA = 32;
glyph.contourEnds = [];
var points: Point[] = 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: number[] = [];
for (i = 0; i < numPoints; i++) {
var flag = file.getUint8();
flags.push(flag);
points.push({
x: 0,
y: 0,
onCurve: (flag & ON_CURVE) > 0
});
if (flag & REPEAT) {
var repeatCount = file.getUint8();
assert(repeatCount > 0);
i += repeatCount;
while (repeatCount--) {
flags.push(flag);
points.push({
x: 0,
y: 0,
onCurve: (flag & ON_CURVE) > 0
});
}
}
}
function readCoords(name: "x" | "y", byteFlag: number,
deltaFlag: number, min: number, max: number) {
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);
}
readCompoundGlyph(file: BinaryReader, glyph: Glyph) {
var ARG_1_AND_2_ARE_WORDS = 1,
ARGS_ARE_XY_VALUES = 2,
ROUND_XY_TO_GRID = 4,
WE_HAVE_A_SCALE = 8,
// RESERVED = 16
MORE_COMPONENTS = 32,
WE_HAVE_AN_X_AND_Y_SCALE = 64,
WE_HAVE_A_TWO_BY_TWO = 128,
WE_HAVE_INSTRUCTIONS = 256,
USE_MY_METRICS = 512,
OVERLAP_COMPONENT = 1024;
var flags = MORE_COMPONENTS;
var component;
glyph.contourEnds = [];
glyph.points = [];
while (flags & MORE_COMPONENTS) {
var arg1, arg2;
flags = file.getUint16();
component = {
glyphIndex: file.getUint16(),
matrix: {
a: 1, b: 0, c: 0, d: 1, e: 0, f: 0
},
destPointIndex: 0,
srcPointIndex: 0
};
if (flags & ARG_1_AND_2_ARE_WORDS) {
arg1 = file.getInt16();
arg2 = file.getInt16();
} else {
arg1 = file.getUint8();
arg2 = file.getUint8();
}
if (flags & ARGS_ARE_XY_VALUES) {
component.matrix.e = arg1;
component.matrix.f = arg2;
} else {
component.destPointIndex = arg1;
component.srcPointIndex = arg2;
}
if (flags & WE_HAVE_A_SCALE) {
component.matrix.a = file.get2Dot14();
component.matrix.d = component.matrix.a;
} else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) {
component.matrix.a = file.get2Dot14();
component.matrix.d = file.get2Dot14();
} else if (flags & WE_HAVE_A_TWO_BY_TWO) {
component.matrix.a = file.get2Dot14();
component.matrix.b = file.get2Dot14();
component.matrix.c = file.get2Dot14();
component.matrix.d = file.get2Dot14();
}
this.log("Read component glyph index %s", component.glyphIndex);
this.log("Transform: [%s %s %s %s %s %s]", component.matrix.a, component.matrix.b,
component.matrix.c, component.matrix.d, component.matrix.e, component.matrix.f);
var old = file.tell();
var simpleGlyph = this.readGlyph(component.glyphIndex);
if (simpleGlyph) {
var pointOffset = glyph.points.length;
for (var i = 0; i < simpleGlyph.contourEnds.length; i++) {
glyph.contourEnds.push(simpleGlyph.contourEnds[i] +
pointOffset);
}
for (i = 0; i < simpleGlyph.points.length; i++) {
var x = simpleGlyph.points[i].x;
var y = simpleGlyph.points[i].y;
x = component.matrix.a * x + component.matrix.b * y +
component.matrix.e;
y = component.matrix.c * x + component.matrix.d * y +
component.matrix.f;
glyph.points.push({
x: x, y: y, onCurve:
simpleGlyph.points[i].onCurve
});
}
}
file.seek(old);
}
glyph.numberOfContours = glyph.contourEnds.length;
if (flags & WE_HAVE_INSTRUCTIONS) {
file.seek(file.getUint16() + file.tell());
}
}
drawGlyph(ctx: CanvasRenderingContext2D, index: number, x: number, y: number) {
var glyph = this.readGlyph(index);
//this.log("Draw GLyph index %s", index);
if (glyph === null) {
return false;
}
var s = 0,
p = 0,
c = 0,
contourStart = 0,
prev;
for (; p < glyph.points.length; p++) {
var point = glyph.points[p];
if (s === 0) {
ctx.moveTo(point.x + x, point.y + y);
s = 1;
} else if (s === 1) {
if (point.onCurve) {
ctx.lineTo(point.x + x, point.y + y);
} else {
s = 2;
}
} else {
prev = glyph.points[p - 1];
if (point.onCurve) {
ctx.quadraticCurveTo(prev.x + x, prev.y + y,
point.x + x, point.y + y);
s = 1;
} else {
ctx.quadraticCurveTo(prev.x + x, prev.y + y,
(prev.x + point.x) / 2 + x,
(prev.y + point.y) / 2 + y);
}
}
if (p === glyph.contourEnds[c]) {
if (s === 2) { // final point was off-curve. connect to start
prev = point;
point = glyph.points[contourStart];
if (point.onCurve) {
ctx.quadraticCurveTo(prev.x + x, prev.y + y,
point.x + x, point.y + y);
} else {
ctx.quadraticCurveTo(prev.x + x, prev.y + y,
(prev.x + point.x) / 2 + x,
(prev.y + point.y) / 2 + y);
}
}
contourStart = p + 1;
c += 1;
s = 0;
}
}
return true;
}
transform(ctx: CanvasRenderingContext2D, size: number) {
ctx.scale(size / this.unitsPerEm, -size / this.unitsPerEm);
}
drawText(ctx: CanvasRenderingContext2D, text: string, x: number, y: number,
size: number) {
ctx.save();
ctx.translate(x, y);
this.transform(ctx, size);
x = 0;
y = 0;
this.resetKern();
for (var i = 0; i < text.length; i++) {
var index = this.mapCode(text.charCodeAt(i));
var metrics = this.getHorizontalMetrics(index);
var kern = this.nextKern(index);
this.log("Metrics for %s code %s index %s: %s %s kern: %s,%s", text.charAt(i),
text.charCodeAt(i), index, metrics.advanceWidth, metrics.leftSideBearing,
kern.x, kern.y);
this.drawGlyph(ctx, index, x + kern.x,//- metrics.leftSideBearing,
y + kern.y);
x += metrics.advanceWidth;
}
ctx.restore();
}
drawSingleGlyph(ctx: CanvasRenderingContext2D, glyphIndex: number,
x: number, y: number, size: number) {
ctx.save();
ctx.translate(x, y);
this.transform(ctx, size);
this.drawGlyph(ctx, glyphIndex, 0, 0);
ctx.restore();
}
mapCode(charCode: number) {
var index = 0;
for (var i = 0; i < this.cmaps.length; i++) {
var cmap = this.cmaps[i];
index = cmap.map(charCode);
if (index) {
break;
}
}
return index;
}
resetKern() {
for (var i = 0; i < this.kern.length; i++) {
this.kern[i].reset();
}
}
nextKern(glyphIndex: number) {
var pt, x = 0, y = 0;
for (var i = 0; i < this.kern.length; i++) {
pt = this.kern[i].get(glyphIndex);
x += pt.x;
y += pt.y;
}
return { x: x, y: y };
}
}
@smhanov
Copy link
Author

smhanov commented May 14, 2020

I have since created a 2000 line version that also reads OpenType fonts with CFF and GPOS tables. But it's getting to the point where you should just use OpenType.js which is very well written and better tested.

@uheinema
Copy link

Hello,

you might find https://github.com/uheinema/forU/blob/master/README.md#foruttf
interesting.
It is more or less a fork of your work, ported to Java/Processing.

All the best and keep up the good work!

Ullrich Heinemann

@smhanov
Copy link
Author

smhanov commented Aug 16, 2022

Some people are using this as a basis for their projects, so I have now posted the version I am using now. It includes support for OpenType and CFF (Adobe type 1) fonts and more CMAP types, so it will handle the majority of Truetype font files. (Still no .woff though). It is in typescript and imports some logging methods so you would have to stub them out to see it run.

@jeancolasp
Copy link

Absolutely love this example. Thanks for having put it together).

@dashxdr
Copy link

dashxdr commented Jul 29, 2023

I have a minor fix for the code inside the composite glyph handler, byte offset values are not handled correctly, they are treated as unsigned. A font I use called cleanFont.ttf has a glyph with a lower case a and above it is a little tilda, but the tilda was way to the right of where it should be. This fixes the problem:

           if (flags & ARGS_ARE_XY_VALUES) {
                component.matrix.e = arg1;
                component.matrix.f = arg2;
            } else {

ought to be

            if (flags & ARGS_ARE_XY_VALUES) {
                if(~flags & ARG_1_AND_2_ARE_WORDS) {
                    if(arg1>127) arg1-=256;
                    if(arg2>127) arg2-=256;
                }
                component.matrix.e = arg1;
                component.matrix.f = arg2;
            } else {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment