Files
jameswitzeman.net/fonts/terminus-font/bin/otb1exp.js
T
2026-05-29 11:30:10 -07:00

896 lines
26 KiB
JavaScript

/*
Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com>
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
'use strict';
const fnutil = require('./fnutil.js');
const fncli = require('./fncli.js');
const bdf = require('./bdf.js');
const bdfexp = require('./bdfexp.js');
const otb1get = require('./otb1get.js');
// -- Table --
const TS_EMPTY = 0;
const TS_SMALL = 64;
const TS_LARGE = 1024;
class Table {
constructor(size, name) {
this.data = Buffer.alloc(size);
this.size = 0;
this.tableName = name;
}
checkSize(size) {
if (size !== this.size) {
throw new Error(`internal error: ${this.tableName} size = ${this.size} instead of ${size}`);
}
}
checksum() {
let cksum = 0;
for (let offset = 0; offset < this.size; offset += 4) {
cksum += this.data.readUInt32BE(offset);
}
return cksum >>> 0;
}
ensure(count) {
if (this.size + count > this.data.length) {
let newSize = this.data.length << 1;
while (this.size + count > newSize) {
newSize <<= 1;
}
const newData = Buffer.alloc(newSize);
this.data.copy(newData, 0, 0, this.size);
this.data = newData;
}
}
get padding() {
return ((this.size + 1) & 3) ^ 1;
}
rewriteUInt32(value, offset) {
this.data.writeUInt32BE(value, offset);
}
write(buffer) {
this.ensure(buffer.length);
buffer.copy(this.data, this.size);
this.size += buffer.length;
}
writeRC(size, writer, name) {
this.ensure(size);
try {
writer(this.size);
} catch (e) {
e.message = e.message.replace('"value"', `"${this.tableName}.${name}"`);
throw e;
}
this.size += size;
}
writeInt8(value, name) {
this.writeRC(1, (offset) => this.data.writeInt8(value, offset), name);
}
writeInt16(value, name) {
this.writeRC(2, (offset) => this.data.writeInt16BE(value, offset), name);
}
writeInt32(value, name) {
this.writeRC(4, (offset) => this.data.writeInt32BE(value, offset), name);
}
writeInt64(value, name) {
this.writeRC(8, (offset) => this.data.writeInt64BE(value, offset), name);
}
writeUInt8(value, name) {
this.writeRC(1, (offset) => this.data.writeUInt8(value, offset), name);
}
writeUInt16(value, name) {
this.writeRC(2, (offset) => this.data.writeUInt16BE(value, offset), name);
}
writeUInt32(value, name) {
this.writeRC(4, (offset) => this.data.writeUInt32BE(value, offset), name);
}
writeUInt48(value, name) {
this.writeUInt16(name, 0);
this.writeRC(6, (offset) => this.data.writeUIntBE(value, offset, 6), name);
}
writeFixed(value, name) {
this.writeRC(4, (offset) => this.data.writeInt32BE(fnutil.round(value * 65536), offset), name);
}
writeTable(table) {
this.write(table.data.slice(0, table.size));
}
}
// -- Params --
const EM_SIZE_MIN = 64;
const EM_SIZE_MAX = 16384;
const EM_SIZE_DEFAULT = 1024;
class Params extends fncli.Params {
constructor() {
super();
this.created = new Date();
this.modified = this.created;
this.dirHint = 0;
this.emSize = EM_SIZE_DEFAULT;
this.lineGap = 0;
this.lowPPem = 0;
this.wLangId = 0x0409;
this.xMaxExtent = true;
this.singleLoca = false;
this.postNames = false;
}
}
// -- Options --
class Options extends fncli.Options {
constructor(needArgs, helpText, versionText) {
super(needArgs.concat(['-d', '-e', '-g', '-l', '-W']), helpText, versionText);
}
parse(name, value, params) {
switch (name) {
case '-d':
params.dirHint = fnutil.parseDec('DIR-HINT', value, -2, 2);
break;
case '-e':
params.emSize = fnutil.parseDec('EM-SIZE', value, EM_SIZE_MIN, EM_SIZE_MAX);
break;
case '-g':
params.lineGap = fnutil.parseDec('LINE-GAP', value, 0, EM_SIZE_MAX << 1);
break;
case '-l':
params.lowPPem = fnutil.parseDec('LOW-PPEM', value, 1, bdf.DPARSE_LIMIT);
break;
case '-W':
params.wLangId = fnutil.parseHex('WLANG-ID', value, 0, 0x7FFF);
break;
case '-X':
params.xMaxExtent = false;
break;
case '-L':
params.singleLoca = true;
break;
case '-P':
params.postNames = true;
break;
default:
this.fallback(name, params);
}
}
}
// -- Font --
class Font extends bdfexp.Font {
constructor(params) {
super();
this.params = params;
this.emAscender = 0;
this.emDescender = 0;
this.emMaxWidth = 0;
this.macStyle = 0;
this.lineSize = 0;
}
get bmpOnly() {
return this.maxCode <= fnutil.UNICODE_BMP_MAX;
}
get created() {
return Font.sfntime(this.params.created);
}
emScale(value, divisor) {
return fnutil.round(value * this.params.emSize / (divisor || this.bbx.height));
}
get italicAngle() {
const value = this.props.get('ITALIC_ANGLE'); // must be integer
return value != null ? fnutil.parseDec('ITALIC_ANGLE', value, -45, 45) : this.italic ? -11.5 : 0;
}
get maxCode() {
return this.chars.slice(-1)[0].code;
}
get minCode() {
return this.chars[0].code;
}
get modified() {
return Font.sfntime(this.params.modified);
}
prepare() {
this.chars.sort((c1, c2) => c1.code - c2.code);
this.chars = this.chars.filter((c, index, array) => index === 0 || c.code !== array[index - 1].code);
this.props.set('CHARS', this.chars.length);
this.emAscender = this.emScale(this.pxAscender);
this.emDescender = this.emAscender - this.params.emSize;
this.emMaxWidth = this.emScaleWidth(this);
this.macStyle = Number(this.bold) + (Number(this.italic) << 1);
this.lineSize = this.emScale(fnutil.round(this.bbx.height / 17) || 1);
}
_read(input) {
super._read(input);
this.prepare();
return this;
}
static read(input, params) {
return (new Font(params))._read(input);
}
emScaleWidth(base) {
return this.emScale(base.bbx.width);
}
static sfntime(stamp) {
return Math.floor((stamp - Date.UTC(1904, 0, 1)) / 1000);
}
get underlinePosition() {
return fnutil.round((this.emDescender + this.lineSize) / 2);
}
get xMaxExtent() {
return this.params.xMaxExtent ? this.emMaxWidth : 0;
}
}
// -- BDAT --
const BDAT_HEADER_SIZE = 4;
const BDAT_METRIC_SIZE = 5;
class BDAT extends Table {
constructor(font) {
super(TS_LARGE, 'EBDT');
// header
this.writeFixed(2, 'version');
// format 1 data
font.chars.forEach(char => {
this.writeUInt8(font.bbx.height, 'height');
this.writeUInt8(char.bbx.width, 'width');
this.writeInt8(0, 'bearingX');
this.writeInt8(font.pxAscender, 'bearingY');
this.writeUInt8(char.bbx.width, 'advance');
this.write(char.data); // imageData
});
}
static getCharSize(char) {
return BDAT_METRIC_SIZE + char.data.length;
}
}
// -- BLOC --
const BLOC_TABLE_SIZE_OFFSET = 12;
const BLOC_PREFIX_SIZE = 0x38; // header 0x08 + 1 bitmapSizeTable * 0x30
const BLOC_INDEX_ARRAY_SIZE = 8; // 1 index record * 0x08
class BLOC extends Table {
constructor(font) {
super(TS_SMALL, 'EBLC');
// header
this.writeFixed(2, 'version');
this.writeUInt32(1, 'numSizes');
// bitmapSizeTable
this.writeUInt32(BLOC_PREFIX_SIZE, 'indexSubTableArrayOffset');
this.writeUInt32(0, 'indexTableSize'); // adjusted later
this.writeUInt32(1, 'numberOfIndexSubTables');
this.writeUInt32(0, 'colorRef');
// hori
this.writeInt8(font.pxAscender, 'hori ascender');
this.writeInt8(font.pxDescender, 'hori descender');
this.writeUInt8(font.bbx.width, 'hori widthMax');
this.writeInt8(1, 'hori caretSlopeNumerator');
this.writeInt8(0, 'hori caretSlopeDenominator');
this.writeInt8(0, 'hori caretOffset');
this.writeInt8(0, 'hori minOriginSB');
this.writeInt8(0, 'hori minAdvanceSB');
this.writeInt8(font.pxAscender, 'hori maxBeforeBL');
this.writeInt8(font.pxDescender, 'hori minAfterBL');
this.writeInt16(0, 'hori padd');
// vert
this.writeInt8(0, 'vert ascender');
this.writeInt8(0, 'vert descender');
this.writeUInt8(0, 'vert widthMax');
this.writeInt8(0, 'vert caretSlopeNumerator');
this.writeInt8(0, 'vert caretSlopeDenominator');
this.writeInt8(0, 'vert caretOffset');
this.writeInt8(0, 'vert minOriginSB');
this.writeInt8(0, 'vert minAdvanceSB');
this.writeInt8(0, 'vert maxBeforeBL');
this.writeInt8(0, 'vert minAfterBL');
this.writeInt16(0, 'vert padd');
// (bitmapSizeTable)
this.writeUInt16(0, 'startGlyphIndex');
this.writeUInt16(font.chars.length - 1, 'endGlyphIndex');
this.writeUInt8(font.bbx.height, 'ppemX');
this.writeUInt8(font.bbx.height, 'ppemY');
this.writeUInt8(1, 'bitDepth');
this.writeUInt8(1, 'flags'); // small metrics are horizontal
// indexSubTableArray
this.writeUInt16(0, 'firstGlyphIndex');
this.writeUInt16(font.chars.length - 1, 'lastGlyphIndex');
this.writeUInt32(BLOC_INDEX_ARRAY_SIZE, 'additionalOffsetToIndexSubtable');
// indexSubtableHeader
this.writeUInt16(font.proportional ? 1 : 2, 'indexFormat');
this.writeUInt16(1, 'imageFormat'); // BDAT -> small metrics, byte-aligned
this.writeUInt32(BDAT_HEADER_SIZE, 'imageDataOffset');
// indexSubtable data
if (font.proportional) {
let offset = 0;
font.chars.forEach(char => {
this.writeUInt32(offset, 'offsetArray[]');
offset += BDAT.getCharSize(char);
});
this.writeUInt32(offset, 'offsetArray[]');
} else {
this.writeUInt32(BDAT.getCharSize(font.chars[0]), 'imageSize');
this.writeUInt8(font.bbx.height, 'height');
this.writeUInt8(font.bbx.width, 'width');
this.writeInt8(0, 'horiBearingX');
this.writeInt8(font.pxAscender, 'horiBearingY');
this.writeUInt8(font.bbx.width, 'horiAdvance');
this.writeInt8(-(font.bbx.width >> 1), 'vertBearingX');
this.writeInt8(0, 'vertBearingY');
this.writeUInt8(font.bbx.height, 'vertAdvance');
}
// adjust
this.rewriteUInt32(this.size - BLOC_PREFIX_SIZE, BLOC_TABLE_SIZE_OFFSET);
}
}
// -- OS/2 --
const OS_2_TABLE_SIZE = 96;
class OS_2 extends Table {
constructor(font) {
super(TS_SMALL, 'OS/2');
// Version 4
const xAvgCharWidth = font.emScale(font.avgWidth); // otb1get.xAvgCharWidth(font);
const ulCharRanges = otb1get.ulCharRanges(font);
const ulCodePages = font.bmpOnly ? otb1get.ulCodePages(font) : [0, 0];
// mostly from FontForge
const scriptXSize = font.emScale(30, 100);
const scriptYSize = font.emScale(40, 100);
const subscriptYOff = scriptYSize >> 1;
const xfactor = Math.tan(font.italicAngle * Math.PI / 180);
const subscriptXOff = 0; // stub, no overlapping characters yet
const superscriptYOff = font.emAscender - scriptYSize;
const superscriptXOff = -fnutil.round(xfactor * superscriptYOff);
// write
this.writeUInt16(4, 'version');
this.writeInt16(xAvgCharWidth, 'xAvgCharWidth');
this.writeUInt16(font.bold ? 700 : 400, 'usWeightClass');
this.writeUInt16(5, 'usWidthClass'); // medium
this.writeInt16(0, 'fsType');
this.writeInt16(scriptXSize, 'ySubscriptXSize');
this.writeInt16(scriptYSize, 'ySubscriptYSize');
this.writeInt16(subscriptXOff, 'ySubscriptXOffset');
this.writeInt16(subscriptYOff, 'ySubscriptYOffset');
this.writeInt16(scriptXSize, 'ySuperscriptXSize');
this.writeInt16(scriptYSize, 'ySuperscriptYSize');
this.writeInt16(superscriptXOff, 'ySuperscriptXOffset');
this.writeInt16(superscriptYOff, 'ySuperscriptYOffset');
this.writeInt16(font.lineSize, 'yStrikeoutSize');
this.writeInt16(font.emScale(25, 100), 'yStrikeoutPosition');
this.writeInt16(0, 'sFamilyClass'); // no classification
this.writeUInt8(2, 'bFamilyType'); // text and display
this.writeUInt8(0, 'bSerifStyle'); // any
this.writeUInt8(font.bold ? 8 : 6, 'bWeight');
this.writeUInt8(font.proportional ? 3 : 9, 'bProportion');
this.writeUInt8(0, 'bContrast');
this.writeUInt8(0, 'bStrokeVariation');
this.writeUInt8(0, 'bArmStyle');
this.writeUInt8(0, 'bLetterform');
this.writeUInt8(0, 'bMidline');
this.writeUInt8(0, 'bXHeight');
this.writeUInt32(ulCharRanges[0], 'ulCharRange1');
this.writeUInt32(ulCharRanges[1], 'ulCharRange2');
this.writeUInt32(ulCharRanges[2], 'ulCharRange3');
this.writeUInt32(ulCharRanges[3], 'ulCharRange4');
this.writeUInt32(0x586F7334, 'achVendID'); // 'Xos4'
this.writeUInt16(OS_2.fsSelection(font), 'fsSelection');
this.writeUInt16(Math.min(font.minCode, fnutil.UNICODE_BMP_MAX), 'firstChar');
this.writeUInt16(Math.min(font.maxCode, fnutil.UNICODE_BMP_MAX), 'lastChar');
this.writeInt16(font.emAscender, 'sTypoAscender');
this.writeInt16(font.emDescender, 'sTypoDescender');
this.writeInt16(font.params.lineGap, 'sTypoLineGap');
this.writeUInt16(font.emAscender, 'usWinAscent');
this.writeUInt16(-font.emDescender, 'usWinDescent');
this.writeUInt32(ulCodePages[0], 'ulCodePageRange1');
this.writeUInt32(ulCodePages[1], 'ulCodePageRange2');
this.writeInt16(font.emScale(font.pxAscender * 0.6), 'sxHeight'); // stub
this.writeInt16(font.emScale(font.pxAscender * 0.8), 'sCapHeight'); // stub
this.writeUInt16(OS_2.defaultChar(font), 'usDefaultChar');
this.writeUInt16(OS_2.breakChar(font), 'usBreakChar');
this.writeUInt16(1, 'usMaxContext');
// check
this.checkSize(OS_2_TABLE_SIZE);
}
static breakChar(font) {
return font.chars.findIndex(char => char.code === 0x20) !== -1 ? 0x20 : font.minCode;
}
static defaultChar(font) {
if (font.defaultCode !== -1 && font.defaultCode <= fnutil.UNICODE_BMP_MAX) {
return font.defaultCode;
}
return font.minCode && font.maxCode;
}
static fsSelection(font) {
const fsSelection = Number(font.bold) * 5 + Number(font.italic);
return fsSelection || (font.xlfd[bdf.XLFD.SLANT] === 'R' ? 0x40 : 0);
}
}
// -- cmap --
const CMAP_4_PREFIX_SIZE = 12;
const CMAP_4_FORMAT_SIZE = 16;
const CMAP_4_SEGMENT_SIZE = 8;
const CMAP_12_PREFIX_SIZE = 20;
const CMAP_12_FORMAT_SIZE = 16;
const CMAP_12_GROUP_SIZE = 12;
class CMapRange {
constructor(glyphIndex = 0, startCode = 0, finalCode = -2) {
this.glyphIndex = glyphIndex;
this.startCode = startCode;
this.finalCode = finalCode;
}
get idDelta() {
return (this.glyphIndex - this.startCode) & 0xFFFF;
}
}
class CMAP extends Table {
constructor(font) {
super(TS_LARGE, 'cmap');
// make ranges
let ranges = [];
let range = new CMapRange();
for (let index = 0; index < font.chars.length; index++) {
let code = font.chars[index].code;
if (code === range.finalCode + 1) {
range.finalCode++;
} else {
range = new CMapRange(index, code, code);
ranges.push(range);
}
}
// write
if (font.bmpOnly) {
if (font.maxCode < 0xFFFF) {
ranges.push(new CMapRange(0, 0xFFFF, 0xFFFF));
}
this.writeFormat4(ranges);
} else {
this.writeFormat12(ranges);
}
}
writeFormat4(ranges) {
// index
this.writeUInt16(0, 'version');
this.writeUInt16(1, 'numberSubtables');
// encoding subtables index
this.writeUInt16(3, 'platformID'); // Microsoft
this.writeUInt16(1, 'platformSpecificID'); // Unicode BMP (UCS-2)
this.writeUInt32(CMAP_4_PREFIX_SIZE, 'offset'); // for Unicode BMP (UCS-2)
// cmap format 4
const segCount = ranges.length;
const subtableSize = CMAP_4_FORMAT_SIZE + segCount * CMAP_4_SEGMENT_SIZE;
const searchRange = 2 << Math.floor(Math.log2(segCount));
this.writeUInt16(4, 'format');
this.writeUInt16(subtableSize, 'length');
this.writeUInt16(0, 'language'); // none/independent
this.writeUInt16(segCount * 2, 'segCountX2');
this.writeUInt16(searchRange, 'searchRange');
this.writeUInt16(Math.log2(searchRange / 2), 'entrySelector');
this.writeUInt16((segCount * 2) - searchRange, 'rangeShift');
ranges.forEach(range => {
this.writeUInt16(range.finalCode, 'endCode');
});
this.writeUInt16(0, 'reservedPad');
ranges.forEach(range => {
this.writeUInt16(range.startCode, 'startCode');
});
ranges.forEach(range => {
this.writeUInt16(range.idDelta, 'idDelta');
});
ranges.forEach(() => this.writeUInt16(0), 'idRangeOffset');
// check
this.checkSize(CMAP_4_PREFIX_SIZE + subtableSize);
}
writeFormat12(ranges) {
// index
this.writeUInt16(0, 'version');
this.writeUInt16(2, 'numberSubtables');
// encoding subtables
this.writeUInt16(0, 'platformID'); // Unicode
this.writeUInt16(4, 'platformSpecificID'); // Unicode 2.0+ full range
this.writeUInt32(CMAP_12_PREFIX_SIZE, 'offset'); // for Unicode 2.0+ full range
this.writeUInt16(3, 'platformID'); // Microsoft
this.writeUInt16(10, 'platformSpecificID'); // Unicode UCS-4
this.writeUInt32(CMAP_12_PREFIX_SIZE, 'offset'); // for Unicode UCS-4
// cmap format 12
const subtableSize = CMAP_12_FORMAT_SIZE + ranges.length * CMAP_12_GROUP_SIZE;
this.writeFixed(12, 'format');
this.writeUInt32(subtableSize, 'length');
this.writeUInt32(0, 'language'); // none/independent
this.writeUInt32(ranges.length, 'nGroups');
this.ranges.forEach(range => {
this.writeUInt32(range.startCode, 'startCharCode');
this.writeUInt32(range.finalCode, 'endCharCode');
this.writeUInt32(range.glyphIndex, 'startGlyphID');
});
// check
this.checkSize(CMAP_12_PREFIX_SIZE + subtableSize);
}
}
// -- glyf --
class GLYF extends Table {
constructor() {
super(TS_EMPTY, 'glyf');
}
}
// -- head --
const HEAD_TABLE_SIZE = 54;
const HEAD_CHECKSUM_OFFSET = 8;
class HEAD extends Table {
constructor(font) {
super(TS_SMALL, 'head');
this.writeFixed(1, 'version');
this.writeFixed(1, 'fontRevision');
this.writeUInt32(0, 'checksumAdjustment'); // adjusted later
this.writeUInt32(0x5F0F3CF5, 'magicNumber');
this.writeUInt16(HEAD.flags(font), 'flags');
this.writeUInt16(font.params.emSize, 'unitsPerEm');
this.writeUInt48(font.created, 'created');
this.writeUInt48(font.modified, 'modified');
this.writeInt16(0, 'xMin');
this.writeInt16(font.emDescender, 'yMin');
this.writeInt16(font.emMaxWidth, 'xMax');
this.writeInt16(font.emAscender, 'yMax');
this.writeUInt16(font.macStyle, 'macStyle');
this.writeUInt16(font.params.lowPPem || font.bbx.height, 'lowestRecPPEM');
this.writeInt16(font.params.dirHint, 'fontDirectionHint');
this.writeInt16(0, 'indexToLocFormat'); // short
this.writeInt16(0, 'glyphDataFormat'); // current
// check
this.checkSize(HEAD_TABLE_SIZE);
}
static flags(font) {
return otb1get.containsRTL(font) ? 0x020B : 0x0B; // y0 base, x0 lsb, scale int
}
}
// -- hhea --
const HHEA_TABLE_SIZE = 36;
class HHEA extends Table {
constructor(font) {
super(TS_SMALL, 'hhea');
this.writeFixed(1, 'version');
this.writeInt16(font.emAscender, 'ascender');
this.writeInt16(font.emDescender, 'descender');
this.writeInt16(font.params.lineGap, 'lineGap');
this.writeUInt16(font.emMaxWidth, 'advanceWidthMax');
this.writeInt16(0, 'minLeftSideBearing');
this.writeInt16(0, 'minRightSideBearing');
this.writeInt16(font.xMaxExtent, 'xMaxExtent');
this.writeInt16(font.italic ? 100 : 1, 'caretSlopeRise');
this.writeInt16(font.italic ? 20 : 0, 'caretSlopeRun');
this.writeInt16(0, 'caretOffset');
this.writeInt16(0, 'reserved');
this.writeInt16(0, 'reserved');
this.writeInt16(0, 'reserved');
this.writeInt16(0, 'reserved');
this.writeInt16(0, 'metricDataFormat'); // current
this.writeUInt16(font.chars.length, 'numOfLongHorMetrics');
// check
this.checkSize(HHEA_TABLE_SIZE);
}
}
// -- hmtx --
class HMTX extends Table {
constructor(font) {
super(TS_LARGE, 'hmtx');
font.chars.forEach(char => {
this.writeUInt16(font.emScaleWidth(char), 'advanceWidth');
this.writeInt16(0, 'leftSideBearing');
});
}
}
// -- loca --
class LOCA extends Table {
constructor(font) {
super(TS_SMALL, 'loca');
if (!font.params.singleLoca) {
font.chars.forEach(() => this.writeUInt16(0, 'offset'));
}
this.writeUInt16(0, 'offset');
}
}
// -- maxp --
const MAXP_TABLE_SIZE = 32;
class MAXP extends Table {
constructor(font) {
super(TS_SMALL, 'maxp');
this.writeFixed(1, 'version');
this.writeUInt16(font.chars.length, 'numGlyphs');
this.writeUInt16(0, 'maxPoints');
this.writeUInt16(0, 'maxContours');
this.writeUInt16(0, 'maxComponentPoints');
this.writeUInt16(0, 'maxComponentContours');
this.writeUInt16(2, 'maxZones');
this.writeUInt16(0, 'maxTwilightPoints');
this.writeUInt16(1, 'maxStorage');
this.writeUInt16(1, 'maxFunctionDefs');
this.writeUInt16(0, 'maxInstructionDefs');
this.writeUInt16(64, 'maxStackElements');
this.writeUInt16(0, 'maxSizeOfInstructions');
this.writeUInt16(0, 'maxComponentElements');
this.writeUInt16(0, 'maxComponentDepth');
// check
this.checkSize(MAXP_TABLE_SIZE);
}
}
// -- name --
const NAME_ID = {
COPYRIGHT: 0,
FONT_FAMILY: 1,
FONT_SUBFAMILY: 2,
UNIQUE_SUBFAMILY: 3,
FULL_FONT_NAME: 4,
LICENSE: 14
};
const NAME_HEADER_SIZE = 6;
const NAME_RECORD_SIZE = 12;
class NAME extends Table {
constructor(font) {
super(TS_LARGE, 'name');
// compute names
let names = new Map();
const copyright = font.props.get('COPYRIGHT');
if (copyright != null) {
names.set(NAME_ID.COPYRIGHT, fnutil.unquote(copyright));
}
const family = font.xlfd[bdf.XLFD.FAMILY_NAME];
const style = ['Regular', 'Bold', 'Italic', 'Bold Italic'][font.macStyle];
names.set(NAME_ID.FONT_FAMILY, family);
names.set(NAME_ID.FONT_SUBFAMILY, style);
names.set(NAME_ID.UNIQUE_SUBFAMILY, `${family} ${style} bitmap height ${font.bbx.height}`);
names.set(NAME_ID.FULL_FONT_NAME, `${family} ${style}`);
let license = font.props.get('LICENSE');
const notice = font.props.get('NOTICE');
if (license == null && notice != null && notice.toLowerCase().includes('license')) {
license = notice;
}
if (license != null) {
names.set(NAME_ID.LICENSE, fnutil.unquote(license));
}
// header
const count = names.size * (1 + 1); // Unicode + Microsoft
const stringOffset = NAME_HEADER_SIZE + NAME_RECORD_SIZE * count;
this.writeUInt16(0, 'format');
this.writeUInt16(count, 'count');
this.writeUInt16(stringOffset, 'stringOffset');
// name records / create values
let values = new Table(TS_LARGE, 'name');
names.forEach((str, nameID) => {
const value = Buffer.from(str, 'utf16le').swap16();
const bmp = font.bmpOnly && value.length === str.length * 2;
// Unicode
this.writeUInt16(0, 'platformID'); // Unicode
this.writeUInt16(bmp ? 3 : 4, 'platformSpecificID');
this.writeUInt16(0, 'languageID');
this.writeUInt16(nameID, 'nameID');
this.writeUInt16(value.length, 'length'); // in bytes
this.writeUInt16(values.size, 'offset');
// Windows
this.writeUInt16(3, 'platformID'); // Microsoft
this.writeUInt16(bmp ? 1 : 10, 'platformSpecificID');
this.writeUInt16(font.params.wLangId, 'languageID');
this.writeUInt16(nameID, 'nameID');
this.writeUInt16(value.length, 'length'); // in bytes
this.writeUInt16(values.size, 'offset');
// value
values.write(value);
});
// write values
this.writeTable(values);
// check
this.checkSize(stringOffset + values.size);
}
}
// -- post --
const POST_TABLE_SIZE = 32;
class POST extends Table {
constructor(font) {
super(TS_SMALL, 'post');
this.writeFixed(font.params.postNames ? 2 : 3, 'format');
this.writeFixed(font.italicAngle, 'italicAngle');
this.writeInt16(font.underlinePosition, 'underlinePosition');
this.writeInt16(font.lineSize, 'underlineThickness');
this.writeUInt32(font.proportional ? 0 : 1, 'isFixedPitch');
this.writeUInt32(0, 'minMemType42');
this.writeUInt32(0, 'maxMemType42');
this.writeUInt32(0, 'minMemType1');
this.writeUInt32(0, 'maxMemType1');
// names
if (font.params.postNames) {
let postNames = otb1get.postMacNames();
const postMacCount = postNames.length;
this.writeUInt16(font.chars.length, 'numberOfGlyphs');
font.chars.forEach(char => {
const name = char.props.get('STARTCHAR');
const index = postNames.indexOf(name);
if (index !== -1) {
this.writeUInt16(index, 'glyphNameIndex');
} else {
this.writeUInt16(postNames.length, 'glyphNameIndex');
postNames.push(name);
}
});
postNames.slice(postMacCount).forEach(name => {
this.writeUInt8(name.length, 'glyphNameLength');
this.write(Buffer.from(name, 'binary'));
});
// check
} else {
this.checkSize(POST_TABLE_SIZE);
}
}
}
// -- SFNT --
const SFNT_HEADER_SIZE = 12;
const SFNT_RECORD_SIZE = 16;
const SFNT_SUBTABLES = [ BDAT, BLOC, OS_2, CMAP, GLYF, HEAD, HHEA, HMTX, LOCA, MAXP, NAME, POST ];
class SFNT extends Table {
constructor(font) {
super(TS_LARGE, 'SFNT');
// create tables
let tables = [];
SFNT_SUBTABLES.forEach(Ctor => {
tables.push(new Ctor(font));
});
// header
const numTables = tables.length;
const entrySelector = Math.floor(Math.log2(numTables));
const searchRange = 16 << entrySelector;
const contentOffset = SFNT_HEADER_SIZE + numTables * SFNT_RECORD_SIZE;
let offset = contentOffset;
let content = new Table(TS_LARGE, 'SFNT');
let headChecksumOffset = -1;
this.writeFixed(1, 'sfntVersion');
this.writeUInt16(numTables, 'numTables');
this.writeUInt16(searchRange, 'searchRange');
this.writeUInt16(entrySelector, 'entrySelector');
this.writeUInt16(numTables * 16 - searchRange, 'rangeShift');
// table records / create content
tables.forEach(table => {
this.write(Buffer.from(table.tableName, 'binary'));
this.writeUInt32(table.checksum(), 'checkSum');
this.writeUInt32(offset, 'offset');
this.writeUInt32(table.size, 'length');
// create content
if (table.tableName === 'head') {
headChecksumOffset = offset + HEAD_CHECKSUM_OFFSET;
}
const paddedSize = table.size + table.padding;
content.write(table.data.slice(0, paddedSize));
offset += paddedSize;
});
// write content
this.writeTable(content);
// check
this.checkSize(contentOffset + content.size);
// adjust
if (headChecksumOffset !== -1) {
this.rewriteUInt32((0xB1B0AFBA - this.checksum()) >>> 0, headChecksumOffset);
}
}
}
// -- Export --
module.exports = Object.freeze({
TS_EMPTY,
TS_SMALL,
TS_LARGE,
Table,
EM_SIZE_MIN,
EM_SIZE_MAX,
EM_SIZE_DEFAULT,
Params,
Options,
Font,
BDAT,
BLOC,
OS_2,
CMAP,
GLYF,
HEAD,
HHEA,
HMTX,
LOCA,
MAXP,
NAME,
POST,
SFNT
});