132 lines
3.7 KiB
JavaScript

// Font class to generate tables
'use strict';
const u = require('../utils');
const debug = require('debug')('font');
const Head = require('./table_head');
const Cmap = require('./table_cmap');
const Glyf = require('./table_glyf');
const Loca = require('./table_loca');
const Kern = require('./table_kern');
class Font {
constructor(fontData, options) {
this.src = fontData;
this.opts = options;
// Map chars to IDs (zero is reserved)
this.glyph_id = { 0: 0 };
this.last_id = 1;
this.createIDs();
debug(`last_id: ${this.last_id}`);
this.init_tables();
this.minY = Math.min(...this.src.glyphs.map(g => g.bbox.y));
debug(`minY: ${this.minY}`);
this.maxY = Math.max(...this.src.glyphs.map(g => g.bbox.y + g.bbox.height));
debug(`maxY: ${this.maxY}`);
// 0 => 1 byte, 1 => 2 bytes
this.glyphIdFormat = Math.max(...Object.values(this.glyph_id)) > 255 ? 1 : 0;
debug(`glyphIdFormat: ${this.glyphIdFormat}`);
// 1.0 by default, will be stored in font as FP12.4
this.kerningScale = 1.0;
let kerningMax = Math.max(...this.src.glyphs.map(g => Object.values(g.kerning).map(Math.abs)).flat());
if (kerningMax >= 7.5) this.kerningScale = Math.ceil(kerningMax / 7.5 * 16) / 16;
debug(`kerningScale: ${this.kerningScale}`);
// 0 => int, 1 => FP4
this.advanceWidthFormat = this.hasKerning() ? 1 : 0;
debug(`advanceWidthFormat: ${this.advanceWidthFormat}`);
this.xy_bits = Math.max(...this.src.glyphs.map(g => Math.max(
u.signed_bits(g.bbox.x), u.signed_bits(g.bbox.y)
)));
debug(`xy_bits: ${this.xy_bits}`);
this.wh_bits = Math.max(...this.src.glyphs.map(g => Math.max(
u.unsigned_bits(g.bbox.width), u.unsigned_bits(g.bbox.height)
)));
debug(`wh_bits: ${this.wh_bits}`);
this.advanceWidthBits = Math.max(...this.src.glyphs.map(
g => u.signed_bits(this.widthToInt(g.advanceWidth))
));
debug(`advanceWidthBits: ${this.advanceWidthBits}`);
let glyphs = this.src.glyphs;
this.monospaced = glyphs.every((v, i, arr) => v.advanceWidth === arr[0].advanceWidth);
debug(`monospaced: ${this.monospaced}`);
// This should stay in the end, because depends on previous variables
// 0 => 2 bytes, 1 => 4 bytes
this.indexToLocFormat = this.glyf.getSize() > 65535 ? 1 : 0;
debug(`indexToLocFormat: ${this.indexToLocFormat}`);
this.subpixels_mode = options.lcd ? 1 : (options.lcd_v ? 2 : 0);
debug(`subpixels_mode: ${this.subpixels_mode}`);
}
init_tables() {
this.head = new Head(this);
this.glyf = new Glyf(this);
this.cmap = new Cmap(this);
this.loca = new Loca(this);
this.kern = new Kern(this);
}
createIDs() {
// Simplified, don't check dupes
this.last_id = 1;
for (let i = 0; i < this.src.glyphs.length; i++) {
// reserve zero for special cases
this.glyph_id[this.src.glyphs[i].code] = this.last_id;
this.last_id++;
}
}
hasKerning() {
if (this.opts.no_kerning) return false;
for (let glyph of this.src.glyphs) {
if (glyph.kerning && Object.keys(glyph.kerning).length) return true;
}
return false;
}
// Returns integer width, depending on format
widthToInt(val) {
if (this.advanceWidthFormat === 0) return Math.round(val);
return Math.round(val * 16);
}
// Convert kerning to FP4.4, useable for writer. Apply `kerningScale`.
kernToFP(val) {
return Math.round(val / this.kerningScale * 16);
}
toBin() {
const result = Buffer.concat([
this.head.toBin(),
this.cmap.toBin(),
this.loca.toBin(),
this.glyf.toBin(),
this.kern.toBin()
]);
debug(`font size: ${result.length}`);
return result;
}
}
module.exports = Font;