896 lines
26 KiB
JavaScript
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
|
|
});
|