opentype.js
Version:
OpenType font parser
441 lines (403 loc) • 15.3 kB
JavaScript
// opentype.js
// https://github.com/opentypejs/opentype.js
// (c) 2015 Frederik De Bleser
// opentype.js may be freely distributed under the MIT license.
/* global DataView, Uint8Array, XMLHttpRequest */
import 'string.prototype.codepointat';
import inflate from 'tiny-inflate';
import Font from './font';
import Glyph from './glyph';
import { CmapEncoding, GlyphNames, addGlyphNames } from './encoding';
import parse from './parse';
import BoundingBox from './bbox';
import Path from './path';
import { nodeBufferToArrayBuffer } from './util';
import cmap from './tables/cmap';
import cff from './tables/cff';
import fvar from './tables/fvar';
import glyf from './tables/glyf';
import gdef from './tables/gdef';
import gpos from './tables/gpos';
import gsub from './tables/gsub';
import head from './tables/head';
import hhea from './tables/hhea';
import hmtx from './tables/hmtx';
import kern from './tables/kern';
import ltag from './tables/ltag';
import loca from './tables/loca';
import maxp from './tables/maxp';
import _name from './tables/name';
import os2 from './tables/os2';
import post from './tables/post';
import meta from './tables/meta';
/**
* The opentype library.
* @namespace opentype
*/
// File loaders /////////////////////////////////////////////////////////
/**
* Loads a font from a file. The callback throws an error message as the first parameter if it fails
* and the font as an ArrayBuffer in the second parameter if it succeeds.
* @param {string} path - The path of the file
* @param {Function} callback - The function to call when the font load completes
*/
function loadFromFile(path, callback) {
const fs = require('fs');
fs.readFile(path, function(err, buffer) {
if (err) {
return callback(err.message);
}
callback(null, nodeBufferToArrayBuffer(buffer));
});
}
/**
* Loads a font from a URL. The callback throws an error message as the first parameter if it fails
* and the font as an ArrayBuffer in the second parameter if it succeeds.
* @param {string} url - The URL of the font file.
* @param {Function} callback - The function to call when the font load completes
*/
function loadFromUrl(url, callback) {
const request = new XMLHttpRequest();
request.open('get', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
if (request.response) {
return callback(null, request.response);
} else {
return callback('Font could not be loaded: ' + request.statusText);
}
};
request.onerror = function () {
callback('Font could not be loaded');
};
request.send();
}
// 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;
}
/**
* Parses WOFF table entries.
* @param {DataView}
* @param {Number}
* @return {Object[]}
*/
function parseWOFFTableEntries(data, numTables) {
const tableEntries = [];
let p = 44; // offset to the first table directory entry.
for (let i = 0; i < numTables; i += 1) {
const tag = parse.getTag(data, p);
const offset = parse.getULong(data, p + 4);
const compLength = parse.getULong(data, p + 8);
const origLength = parse.getULong(data, p + 12);
let compression;
if (compLength < origLength) {
compression = 'WOFF';
} else {
compression = false;
}
tableEntries.push({tag: tag, offset: offset, compression: compression,
compressedLength: compLength, length: origLength});
p += 20;
}
return tableEntries;
}
/**
* @typedef TableData
* @type Object
* @property {DataView} data - The DataView
* @property {number} offset - The data offset.
*/
/**
* @param {DataView}
* @param {Object}
* @return {TableData}
*/
function uncompressTable(data, tableEntry) {
if (tableEntry.compression === 'WOFF') {
const inBuffer = new Uint8Array(data.buffer, tableEntry.offset + 2, tableEntry.compressedLength - 2);
const outBuffer = new Uint8Array(tableEntry.length);
inflate(inBuffer, outBuffer);
if (outBuffer.byteLength !== tableEntry.length) {
throw new Error('Decompression error: ' + tableEntry.tag + ' decompressed length doesn\'t match recorded length');
}
const view = new DataView(outBuffer.buffer, 0);
return {data: view, offset: 0};
} else {
return {data: data, offset: tableEntry.offset};
}
}
// 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) {
opt = (opt === undefined || opt === null) ? {} : opt;
let indexToLocFormat;
let ltagTable;
// Since the constructor can also be called to create new fonts from scratch, we indicate this
// should be an empty font that we'll fill with our own data.
const font = new Font({empty: true});
// OpenType fonts use big endian byte ordering.
// We can't rely on typed array view types, because they operate with the endianness of the host computer.
// Instead we use DataViews where we can specify endianness.
const data = new DataView(buffer, 0);
let numTables;
let tableEntries = [];
const signature = parse.getTag(data, 0);
if (signature === String.fromCharCode(0, 1, 0, 0) || signature === 'true' || signature === 'typ1') {
font.outlinesFormat = 'truetype';
numTables = parse.getUShort(data, 4);
tableEntries = parseOpenTypeTableEntries(data, numTables);
} else if (signature === 'OTTO') {
font.outlinesFormat = 'cff';
numTables = parse.getUShort(data, 4);
tableEntries = parseOpenTypeTableEntries(data, numTables);
} else if (signature === 'wOFF') {
const flavor = parse.getTag(data, 4);
if (flavor === String.fromCharCode(0, 1, 0, 0)) {
font.outlinesFormat = 'truetype';
} else if (flavor === 'OTTO') {
font.outlinesFormat = 'cff';
} else {
throw new Error('Unsupported OpenType flavor ' + signature);
}
numTables = parse.getUShort(data, 12);
tableEntries = parseWOFFTableEntries(data, numTables);
} else {
throw new Error('Unsupported OpenType signature ' + signature);
}
let cffTableEntry;
let fvarTableEntry;
let glyfTableEntry;
let gdefTableEntry;
let gposTableEntry;
let gsubTableEntry;
let hmtxTableEntry;
let kernTableEntry;
let locaTableEntry;
let nameTableEntry;
let metaTableEntry;
let p;
for (let i = 0; i < numTables; i += 1) {
const tableEntry = tableEntries[i];
let table;
switch (tableEntry.tag) {
case 'cmap':
table = uncompressTable(data, tableEntry);
font.tables.cmap = cmap.parse(table.data, table.offset);
font.encoding = new CmapEncoding(font.tables.cmap);
break;
case 'cvt ' :
table = uncompressTable(data, tableEntry);
p = new parse.Parser(table.data, table.offset);
font.tables.cvt = p.parseShortList(tableEntry.length / 2);
break;
case 'fvar':
fvarTableEntry = tableEntry;
break;
case 'fpgm' :
table = uncompressTable(data, tableEntry);
p = new parse.Parser(table.data, table.offset);
font.tables.fpgm = p.parseByteList(tableEntry.length);
break;
case 'head':
table = uncompressTable(data, tableEntry);
font.tables.head = head.parse(table.data, table.offset);
font.unitsPerEm = font.tables.head.unitsPerEm;
indexToLocFormat = font.tables.head.indexToLocFormat;
break;
case 'hhea':
table = uncompressTable(data, tableEntry);
font.tables.hhea = hhea.parse(table.data, table.offset);
font.ascender = font.tables.hhea.ascender;
font.descender = font.tables.hhea.descender;
font.numberOfHMetrics = font.tables.hhea.numberOfHMetrics;
break;
case 'hmtx':
hmtxTableEntry = tableEntry;
break;
case 'ltag':
table = uncompressTable(data, tableEntry);
ltagTable = ltag.parse(table.data, table.offset);
break;
case 'maxp':
table = uncompressTable(data, tableEntry);
font.tables.maxp = maxp.parse(table.data, table.offset);
font.numGlyphs = font.tables.maxp.numGlyphs;
break;
case 'name':
nameTableEntry = tableEntry;
break;
case 'OS/2':
table = uncompressTable(data, tableEntry);
font.tables.os2 = os2.parse(table.data, table.offset);
break;
case 'post':
table = uncompressTable(data, tableEntry);
font.tables.post = post.parse(table.data, table.offset);
font.glyphNames = new GlyphNames(font.tables.post);
break;
case 'prep' :
table = uncompressTable(data, tableEntry);
p = new parse.Parser(table.data, table.offset);
font.tables.prep = p.parseByteList(tableEntry.length);
break;
case 'glyf':
glyfTableEntry = tableEntry;
break;
case 'loca':
locaTableEntry = tableEntry;
break;
case 'CFF ':
cffTableEntry = tableEntry;
break;
case 'kern':
kernTableEntry = tableEntry;
break;
case 'GDEF':
gdefTableEntry = tableEntry;
break;
case 'GPOS':
gposTableEntry = tableEntry;
break;
case 'GSUB':
gsubTableEntry = tableEntry;
break;
case 'meta':
metaTableEntry = tableEntry;
break;
}
}
const nameTable = uncompressTable(data, nameTableEntry);
font.tables.name = _name.parse(nameTable.data, nameTable.offset, ltagTable);
font.names = font.tables.name;
if (glyfTableEntry && locaTableEntry) {
const shortVersion = indexToLocFormat === 0;
const locaTable = uncompressTable(data, locaTableEntry);
const locaOffsets = loca.parse(locaTable.data, locaTable.offset, font.numGlyphs, shortVersion);
const glyfTable = uncompressTable(data, glyfTableEntry);
font.glyphs = glyf.parse(glyfTable.data, glyfTable.offset, locaOffsets, font, opt);
} else if (cffTableEntry) {
const cffTable = uncompressTable(data, cffTableEntry);
cff.parse(cffTable.data, cffTable.offset, font, opt);
} else {
throw new Error('Font doesn\'t contain TrueType or CFF outlines.');
}
const hmtxTable = uncompressTable(data, hmtxTableEntry);
hmtx.parse(font, hmtxTable.data, hmtxTable.offset, font.numberOfHMetrics, font.numGlyphs, font.glyphs, opt);
addGlyphNames(font, opt);
if (kernTableEntry) {
const kernTable = uncompressTable(data, kernTableEntry);
font.kerningPairs = kern.parse(kernTable.data, kernTable.offset);
} else {
font.kerningPairs = {};
}
if (gdefTableEntry) {
const gdefTable = uncompressTable(data, gdefTableEntry);
font.tables.gdef = gdef.parse(gdefTable.data, gdefTable.offset);
}
if (gposTableEntry) {
const gposTable = uncompressTable(data, gposTableEntry);
font.tables.gpos = gpos.parse(gposTable.data, gposTable.offset);
font.position.init();
}
if (gsubTableEntry) {
const gsubTable = uncompressTable(data, gsubTableEntry);
font.tables.gsub = gsub.parse(gsubTable.data, gsubTable.offset);
}
if (fvarTableEntry) {
const fvarTable = uncompressTable(data, fvarTableEntry);
font.tables.fvar = fvar.parse(fvarTable.data, fvarTable.offset, font.names);
}
if (metaTableEntry) {
const metaTable = uncompressTable(data, metaTableEntry);
font.tables.meta = meta.parse(metaTable.data, metaTable.offset);
font.metas = font.tables.meta;
}
return font;
}
/**
* Asynchronously load the font from a URL or a filesystem. When done, call the callback
* with two arguments `(err, font)`. The `err` will be null on success,
* the `font` is a Font object.
* We use the node.js callback convention so that
* opentype.js can integrate with frameworks like async.js.
* @alias opentype.load
* @param {string} url - The URL of the font to load.
* @param {Function} callback - The callback.
*/
function load(url, callback, opt) {
opt = (opt === undefined || opt === null) ? {} : opt;
const isNode = typeof window === 'undefined';
const loadFn = isNode && !opt.isUrl ? loadFromFile : loadFromUrl;
return new Promise((resolve, reject) => {
loadFn(url, function(err, arrayBuffer) {
if (err) {
if (callback) {
return callback(err);
} else {
reject(err);
}
}
let font;
try {
font = parseBuffer(arrayBuffer, opt);
} catch (e) {
if (callback) {
return callback(e, null);
} else {
reject(e);
}
}
if (callback) {
return callback(null, font);
} else {
resolve(font);
}
});
});
}
/**
* Synchronously load the font from a URL or file.
* When done, returns the font object or throws an error.
* @alias opentype.loadSync
* @param {string} url - The URL of the font to load.
* @param {Object} opt - opt.lowMemory
* @return {opentype.Font}
*/
function loadSync(url, opt) {
const fs = require('fs');
const buffer = fs.readFileSync(url);
return parseBuffer(nodeBufferToArrayBuffer(buffer), opt);
}
export {
Font,
Glyph,
Path,
BoundingBox,
parse as _parse,
parseBuffer as parse,
load,
loadSync
};