Add mem free printing (I need it to verify a suspicion relating to a bug)

This commit is contained in:
2024-04-22 13:28:24 -05:00
parent 20cd951cde
commit c60251359c
120 changed files with 31903 additions and 2 deletions

View File

@@ -0,0 +1,105 @@
// Find an optimal configuration of cmap tables representing set of codepoints,
// using simple breadth-first algorithm
//
// Assume that:
// - codepoints have one-to-one correspondence to glyph ids
// - glyph ids are always bigger for bigger codepoints
// - glyph ids are always consecutive (1..N without gaps)
//
// This way we can omit glyph ids from all calculations entirely: if codepoints
// fit in format0, then glyph ids also will.
//
// format6 is not considered, because if glyph ids can be delta-coded,
// multiple format0 tables are guaranteed to be smaller than a single format6.
//
// sparse format is not used because as long as glyph ids are consecutive,
// sparse_tiny will always be preferred.
//
'use strict';
function estimate_format0_tiny_size(/*start_code, end_code*/) {
return 16;
}
function estimate_format0_size(start_code, end_code) {
return 16 + (end_code - start_code + 1);
}
//function estimate_sparse_size(count) {
// return 16 + count * 4;
//}
function estimate_sparse_tiny_size(count) {
return 16 + count * 2;
}
module.exports = function cmap_split(all_codepoints) {
all_codepoints = all_codepoints.sort((a, b) => a - b);
let min_paths = [];
for (let i = 0; i < all_codepoints.length; i++) {
let min = { dist: Infinity };
for (let j = 0; j <= i; j++) {
let prev_dist = (j - 1 >= 0) ? min_paths[j - 1].dist : 0;
let s;
if (all_codepoints[i] - all_codepoints[j] < 256) {
s = estimate_format0_size(all_codepoints[j], all_codepoints[i]);
/* eslint-disable max-depth */
if (prev_dist + s < min.dist) {
min = {
dist: prev_dist + s,
start: j,
end: i,
format: 'format0'
};
}
}
if (all_codepoints[i] - all_codepoints[j] < 256 && all_codepoints[i] - i === all_codepoints[j] - j) {
s = estimate_format0_tiny_size(all_codepoints[j], all_codepoints[i]);
/* eslint-disable max-depth */
if (prev_dist + s < min.dist) {
min = {
dist: prev_dist + s,
start: j,
end: i,
format: 'format0_tiny'
};
}
}
// tiny sparse will always be preferred over full sparse because glyph ids are consecutive
if (all_codepoints[i] - all_codepoints[j] < 65536) {
s = estimate_sparse_tiny_size(i - j + 1);
if (prev_dist + s < min.dist) {
min = {
dist: prev_dist + s,
start: j,
end: i,
format: 'sparse_tiny'
};
}
}
}
min_paths[i] = min;
}
let result = [];
for (let i = all_codepoints.length - 1; i >= 0;) {
let path = min_paths[i];
result.unshift([ path.format, all_codepoints.slice(path.start, path.end + 1) ]);
i = path.start - 1;
}
return result;
};

107
node_modules/lv_font_conv/lib/font/compress.js generated vendored Normal file
View File

@@ -0,0 +1,107 @@
'use strict';
//const debug = require('debug')('compress');
function count_same(arr, offset) {
let same = 1;
let val = arr[offset];
for (let i = offset + 1; i < arr.length; i++) {
if (arr[i] !== val) break;
same++;
}
return same;
}
//
// Compress pixels with RLE-like algorythm (modified I3BN)
//
// 1. Require minimal repeat count (1) to enter I3BN mode
// 2. Increased 1-bit-replaced repeat limit (2 => 10)
// 3. Length of direct repetition counter reduced (8 => 6 bits).
//
// pixels - flat array of pixels (one per entry)
// options.bpp - bits per pixels
//
module.exports = function compress(bitStream, pixels, options) {
const opts = Object.assign({}, { repeat: 1 }, options);
// Minimal repetitions count to enable RLE mode.
const RLE_SKIP_COUNT = 1;
// Number of repeats, when `1` used to replace data
// If more - write as number
const RLE_BIT_COLLAPSED_COUNT = 10;
const RLE_COUNTER_BITS = 6; // (2^bits - 1) - max value
const RLE_COUNTER_MAX = (1 << RLE_COUNTER_BITS) - 1;
// Force flush if counter dencity exceeded.
const RLE_MAX_REPEATS = RLE_COUNTER_MAX + RLE_BIT_COLLAPSED_COUNT + 1;
//let bits_start_offset = bitStream.index;
let offset = 0;
while (offset < pixels.length) {
const p = pixels[offset];
let same = count_same(pixels, offset);
// Clamp value because RLE counter density is limited
if (same > RLE_MAX_REPEATS + RLE_SKIP_COUNT) {
same = RLE_MAX_REPEATS + RLE_SKIP_COUNT;
}
//debug(`offset: ${offset}, count: ${same}, pixel: ${p}`);
offset += same;
// If not enough for RLE - write as is.
if (same <= RLE_SKIP_COUNT) {
for (let i = 0; i < same; i++) {
bitStream.writeBits(p, opts.bpp);
//debug(`==> ${opts.bpp} bits`);
}
continue;
}
// First, write "skipped" head as is.
for (let i = 0; i < RLE_SKIP_COUNT; i++) {
bitStream.writeBits(p, opts.bpp);
//debug(`==> ${opts.bpp} bits`);
}
same -= RLE_SKIP_COUNT;
// Not reached state to use counter => dump bit-extended
if (same <= RLE_BIT_COLLAPSED_COUNT) {
bitStream.writeBits(p, opts.bpp);
//debug(`==> ${opts.bpp} bits (val)`);
for (let i = 0; i < same; i++) {
/*eslint-disable max-depth*/
if (i < same - 1) {
bitStream.writeBits(1, 1);
//debug('==> 1 bit (rle repeat)');
} else {
bitStream.writeBits(0, 1);
//debug('==> 1 bit (rle repeat last)');
}
}
continue;
}
same -= RLE_BIT_COLLAPSED_COUNT + 1;
bitStream.writeBits(p, opts.bpp);
//debug(`==> ${opts.bpp} bits (val)`);
for (let i = 0; i < RLE_BIT_COLLAPSED_COUNT + 1; i++) {
bitStream.writeBits(1, 1);
//debug('==> 1 bit (rle repeat)');
}
bitStream.writeBits(same, RLE_COUNTER_BITS);
//debug(`==> 4 bits (rle repeat count ${same})`);
}
//debug(`output bits: ${bitStream.index - bits_start_offset}`);
};

131
node_modules/lv_font_conv/lib/font/font.js generated vendored Normal file
View File

@@ -0,0 +1,131 @@
// 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;

201
node_modules/lv_font_conv/lib/font/table_cmap.js generated vendored Normal file
View File

@@ -0,0 +1,201 @@
'use strict';
const build_subtables = require('./cmap_build_subtables');
const u = require('../utils');
const debug = require('debug')('font.table.cmap');
const O_SIZE = 0;
const O_LABEL = O_SIZE + 4;
const O_COUNT = O_LABEL + 4;
const HEAD_LENGTH = O_COUNT + 4;
const SUB_FORMAT_0 = 0;
const SUB_FORMAT_0_TINY = 2;
const SUB_FORMAT_SPARSE = 1;
const SUB_FORMAT_SPARSE_TINY = 3;
class Cmap {
constructor(font) {
this.font = font;
this.label = 'cmap';
this.sub_heads = [];
this.sub_data = [];
this.compiled = false;
}
compile() {
if (this.compiled) return;
this.compiled = true;
const f = this.font;
let subtables_plan = build_subtables(f.src.glyphs.map(g => g.code));
const count_format0 = subtables_plan.filter(s => s[0] === 'format0').length;
const count_sparse = subtables_plan.length - count_format0;
debug(`${subtables_plan.length} subtable(s): ${count_format0} "format 0", ${count_sparse} "sparse"`);
for (let [ format, codepoints ] of subtables_plan) {
let g = this.glyphByCode(codepoints[0]);
let start_glyph_id = f.glyph_id[g.code];
let min_code = codepoints[0];
let max_code = codepoints[codepoints.length - 1];
let entries_count = max_code - min_code + 1;
let format_code = 0;
if (format === 'format0_tiny') {
format_code = SUB_FORMAT_0_TINY;
this.sub_data.push(Buffer.alloc(0));
} else if (format === 'format0') {
format_code = SUB_FORMAT_0;
this.sub_data.push(this.create_format0_data(min_code, max_code, start_glyph_id));
} else if (format === 'sparse_tiny') {
entries_count = codepoints.length;
format_code = SUB_FORMAT_SPARSE_TINY;
this.sub_data.push(this.create_sparse_tiny_data(codepoints, start_glyph_id));
} else { // assume format === 'sparse'
entries_count = codepoints.length;
format_code = SUB_FORMAT_SPARSE;
this.sub_data.push(this.create_sparse_data(codepoints, start_glyph_id));
}
this.sub_heads.push(this.createSubHeader(
min_code,
max_code - min_code + 1,
start_glyph_id,
entries_count,
format_code
));
}
this.subHeaderUpdateAllOffsets();
}
createSubHeader(rangeStart, rangeLen, glyphIdOffset, total, type) {
const buf = Buffer.alloc(16);
// buf.writeUInt32LE(offset, 0); offset unknown at this moment
buf.writeUInt32LE(rangeStart, 4);
buf.writeUInt16LE(rangeLen, 8);
buf.writeUInt16LE(glyphIdOffset, 10);
buf.writeUInt16LE(total, 12);
buf.writeUInt8(type, 14);
return buf;
}
subHeaderUpdateOffset(header, val) {
header.writeUInt32LE(val, 0);
}
subHeaderUpdateAllOffsets() {
for (let i = 0; i < this.sub_heads.length; i++) {
const offset = HEAD_LENGTH +
u.sum(this.sub_heads.map(h => h.length)) +
u.sum(this.sub_data.slice(0, i).map(d => d.length));
this.subHeaderUpdateOffset(this.sub_heads[i], offset);
}
}
glyphByCode(code) {
for (let g of this.font.src.glyphs) {
if (g.code === code) return g;
}
return null;
}
collect_format0_data(min_code, max_code, start_glyph_id) {
let data = [];
for (let i = min_code; i <= max_code; i++) {
const g = this.glyphByCode(i);
if (!g) {
data.push(0);
continue;
}
const id_delta = this.font.glyph_id[g.code] - start_glyph_id;
if (id_delta < 0 || id_delta > 255) throw new Error('Glyph ID delta out of Format 0 range');
data.push(id_delta);
}
return data;
}
create_format0_data(min_code, max_code, start_glyph_id) {
const data = this.collect_format0_data(min_code, max_code, start_glyph_id);
return u.balign4(Buffer.from(data));
}
collect_sparse_data(codepoints, start_glyph_id) {
let codepoints_list = [];
let ids_list = [];
for (let code of codepoints) {
let g = this.glyphByCode(code);
let id = this.font.glyph_id[g.code];
let code_delta = code - codepoints[0];
let id_delta = id - start_glyph_id;
if (code_delta < 0 || code_delta > 65535) throw new Error('Codepoint delta out of range');
if (id_delta < 0 || id_delta > 65535) throw new Error('Glyph ID delta out of range');
codepoints_list.push(code_delta);
ids_list.push(id_delta);
}
return {
codes: codepoints_list,
ids: ids_list
};
}
create_sparse_data(codepoints, start_glyph_id) {
const data = this.collect_sparse_data(codepoints, start_glyph_id);
return u.balign4(Buffer.concat([
u.bFromA16(data.codes),
u.bFromA16(data.ids)
]));
}
create_sparse_tiny_data(codepoints, start_glyph_id) {
const data = this.collect_sparse_data(codepoints, start_glyph_id);
return u.balign4(u.bFromA16(data.codes));
}
toBin() {
if (!this.compiled) this.compile();
const buf = Buffer.concat([
Buffer.alloc(HEAD_LENGTH),
Buffer.concat(this.sub_heads),
Buffer.concat(this.sub_data)
]);
debug(`table size = ${buf.length}`);
buf.writeUInt32LE(buf.length, O_SIZE);
buf.write(this.label, O_LABEL);
buf.writeUInt32LE(this.sub_heads.length, O_COUNT);
return buf;
}
}
module.exports = Cmap;

147
node_modules/lv_font_conv/lib/font/table_glyf.js generated vendored Normal file
View File

@@ -0,0 +1,147 @@
'use strict';
const u = require('../utils');
const { BitStream } = require('bit-buffer');
const debug = require('debug')('font.table.glyf');
const compress = require('./compress');
const O_SIZE = 0;
const O_LABEL = O_SIZE + 4;
const HEAD_LENGTH = O_LABEL + 4;
class Glyf {
constructor(font) {
this.font = font;
this.label = 'glyf';
this.compiled = false;
this.binData = [];
}
// convert 8-bit opacity to bpp-bit
pixelsToBpp(pixels) {
const bpp = this.font.opts.bpp;
return pixels.map(line => line.map(p => (p >>> (8 - bpp))));
}
// Returns "binary stream" (Buffer) of compiled glyph data
compileGlyph(glyph) {
// Allocate memory, enough for eny storage formats
const buf = Buffer.alloc(100 + glyph.bbox.width * glyph.bbox.height * 4);
const bs = new BitStream(buf);
bs.bigEndian = true;
const f = this.font;
// Store Width
if (!f.monospaced) {
let w = f.widthToInt(glyph.advanceWidth);
bs.writeBits(w, f.advanceWidthBits);
}
// Store X, Y
bs.writeBits(glyph.bbox.x, f.xy_bits);
bs.writeBits(glyph.bbox.y, f.xy_bits);
bs.writeBits(glyph.bbox.width, f.wh_bits);
bs.writeBits(glyph.bbox.height, f.wh_bits);
const pixels = this.pixelsToBpp(glyph.pixels);
this.storePixels(bs, pixels);
// Shrink size
const result = Buffer.alloc(bs.byteIndex);
buf.copy(result, 0, 0, bs.byteIndex);
return result;
}
storePixels(bitStream, pixels) {
if (this.getCompressionCode() === 0) this.storePixelsRaw(bitStream, pixels);
else this.storePixelsCompressed(bitStream, pixels);
}
storePixelsRaw(bitStream, pixels) {
const bpp = this.font.opts.bpp;
for (let y = 0; y < pixels.length; y++) {
const line = pixels[y];
for (let x = 0; x < line.length; x++) {
bitStream.writeBits(line[x], bpp);
}
}
}
storePixelsCompressed(bitStream, pixels) {
let p;
if (this.font.opts.no_prefilter) p = pixels.flat();
else p = u.prefilter(pixels).flat();
compress(bitStream, p, this.font.opts);
}
// Create internal struct with binary data for each glyph
// Needed to calculate offsets & build final result
compile() {
this.compiled = true;
this.binData = [
Buffer.alloc(0) // Reserve id 0
];
const f = this.font;
f.src.glyphs.forEach(g => {
const id = f.glyph_id[g.code];
this.binData[id] = this.compileGlyph(g);
});
}
toBin() {
if (!this.compiled) this.compile();
const buf = u.balign4(Buffer.concat([
Buffer.alloc(HEAD_LENGTH),
Buffer.concat(this.binData)
]));
buf.writeUInt32LE(buf.length, O_SIZE);
buf.write(this.label, O_LABEL);
debug(`table size = ${buf.length}`);
return buf;
}
getSize() {
if (!this.compiled) this.compile();
return u.align4(HEAD_LENGTH + u.sum(this.binData.map(b => b.length)));
}
getOffset(id) {
if (!this.compiled) this.compile();
let offset = HEAD_LENGTH;
for (let i = 0; i < id; i++) offset += this.binData[i].length;
return offset;
}
getCompressionCode() {
if (this.font.opts.no_compress) return 0;
if (this.font.opts.bpp === 1) return 0;
if (this.font.opts.no_prefilter) return 2;
return 1;
}
}
module.exports = Glyf;

99
node_modules/lv_font_conv/lib/font/table_head.js generated vendored Normal file
View File

@@ -0,0 +1,99 @@
'use strict';
const u = require('../utils');
const debug = require('debug')('font.table.head');
const O_SIZE = 0;
const O_LABEL = O_SIZE + 4;
const O_VERSION = O_LABEL + 4;
const O_TABLES = O_VERSION + 4;
const O_FONT_SIZE = O_TABLES + 2;
const O_ASCENT = O_FONT_SIZE + 2;
const O_DESCENT = O_ASCENT + 2;
const O_TYPO_ASCENT = O_DESCENT + 2;
const O_TYPO_DESCENT = O_TYPO_ASCENT + 2;
const O_TYPO_LINE_GAP = O_TYPO_DESCENT + 2;
const O_MIN_Y = O_TYPO_LINE_GAP + 2;
const O_MAX_Y = O_MIN_Y + 2;
const O_DEF_ADVANCE_WIDTH = O_MAX_Y + 2;
const O_KERNING_SCALE = O_DEF_ADVANCE_WIDTH + 2;
const O_INDEX_TO_LOC_FORMAT = O_KERNING_SCALE + 2;
const O_GLYPH_ID_FORMAT = O_INDEX_TO_LOC_FORMAT + 1;
const O_ADVANCE_WIDTH_FORMAT = O_GLYPH_ID_FORMAT + 1;
const O_BITS_PER_PIXEL = O_ADVANCE_WIDTH_FORMAT + 1;
const O_XY_BITS = O_BITS_PER_PIXEL + 1;
const O_WH_BITS = O_XY_BITS + 1;
const O_ADVANCE_WIDTH_BITS = O_WH_BITS + 1;
const O_COMPRESSION_ID = O_ADVANCE_WIDTH_BITS + 1;
const O_SUBPIXELS_MODE = O_COMPRESSION_ID + 1;
const O_TMP_RESERVED1 = O_SUBPIXELS_MODE + 1;
const O_UNDERLINE_POSITION = O_TMP_RESERVED1 + 1;
const O_UNDERLINE_THICKNESS = O_UNDERLINE_POSITION + 2;
const HEAD_LENGTH = u.align4(O_UNDERLINE_THICKNESS + 2);
class Head {
constructor(font) {
this.font = font;
this.label = 'head';
this.version = 1;
}
toBin() {
const buf = Buffer.alloc(HEAD_LENGTH);
debug(`table size = ${buf.length}`);
buf.writeUInt32LE(HEAD_LENGTH, O_SIZE);
buf.write(this.label, O_LABEL);
buf.writeUInt32LE(this.version, O_VERSION);
const f = this.font;
const tables_count = f.hasKerning() ? 4 : 3;
buf.writeUInt16LE(tables_count, O_TABLES);
buf.writeUInt16LE(f.src.size, O_FONT_SIZE);
buf.writeUInt16LE(f.src.ascent, O_ASCENT);
buf.writeInt16LE(f.src.descent, O_DESCENT);
buf.writeUInt16LE(f.src.typoAscent, O_TYPO_ASCENT);
buf.writeInt16LE(f.src.typoDescent, O_TYPO_DESCENT);
buf.writeUInt16LE(f.src.typoLineGap, O_TYPO_LINE_GAP);
buf.writeInt16LE(f.minY, O_MIN_Y);
buf.writeInt16LE(f.maxY, O_MAX_Y);
if (f.monospaced) {
buf.writeUInt16LE(f.widthToInt(f.src.glyphs[0].advanceWidth), O_DEF_ADVANCE_WIDTH);
} else {
buf.writeUInt16LE(0, O_DEF_ADVANCE_WIDTH);
}
buf.writeUInt16LE(Math.round(f.kerningScale * 16), O_KERNING_SCALE); // FP12.4
buf.writeUInt8(f.indexToLocFormat, O_INDEX_TO_LOC_FORMAT);
buf.writeUInt8(f.glyphIdFormat, O_GLYPH_ID_FORMAT);
buf.writeUInt8(f.advanceWidthFormat, O_ADVANCE_WIDTH_FORMAT);
buf.writeUInt8(f.opts.bpp, O_BITS_PER_PIXEL);
buf.writeUInt8(f.xy_bits, O_XY_BITS);
buf.writeUInt8(f.wh_bits, O_WH_BITS);
if (f.monospaced) buf.writeUInt8(0, O_ADVANCE_WIDTH_BITS);
else buf.writeUInt8(f.advanceWidthBits, O_ADVANCE_WIDTH_BITS);
buf.writeUInt8(f.glyf.getCompressionCode(), O_COMPRESSION_ID);
buf.writeUInt8(f.subpixels_mode, O_SUBPIXELS_MODE);
buf.writeInt16LE(f.src.underlinePosition, O_UNDERLINE_POSITION);
buf.writeUInt16LE(f.src.underlineThickness, O_UNDERLINE_POSITION);
return buf;
}
}
module.exports = Head;

256
node_modules/lv_font_conv/lib/font/table_kern.js generated vendored Normal file
View File

@@ -0,0 +1,256 @@
'use strict';
const u = require('../utils');
const debug = require('debug')('font.table.kern');
const O_SIZE = 0;
const O_LABEL = O_SIZE + 4;
const O_FORMAT = O_LABEL + 4;
const HEAD_LENGTH = u.align4(O_FORMAT + 1);
class Kern {
constructor(font) {
this.font = font;
this.label = 'kern';
this.format3_forced = false;
}
collect_format0_data() {
const f = this.font;
const glyphs = u.sort_by(this.font.src.glyphs, g => f.glyph_id[g.code]);
const kernSorted = [];
for (let g of glyphs) {
if (!g.kerning || !Object.keys(g.kerning).length) continue;
const glyph_id = f.glyph_id[g.code];
const paired = u.sort_by(Object.keys(g.kerning), code => f.glyph_id[code]);
for (let code of paired) {
const glyph_id2 = f.glyph_id[code];
kernSorted.push([ glyph_id, glyph_id2, g.kerning[code] ]);
}
}
return kernSorted;
}
create_format0_data() {
const f = this.font;
const glyphs = this.font.src.glyphs;
const kernSorted = this.collect_format0_data();
const count = kernSorted.length;
const kerned_glyphs = glyphs.filter(g => Object.keys(g.kerning).length).length;
const kerning_list_max = Math.max(...glyphs.map(g => Object.keys(g.kerning).length));
debug(`${kerned_glyphs} kerned glyphs of ${glyphs.length}, ${kerning_list_max} max list, ${count} total pairs`);
const subheader = Buffer.alloc(4);
subheader.writeUInt32LE(count, 0);
const pairs_buf = Buffer.alloc((f.glyphIdFormat ? 4 : 2) * count);
// Write kerning pairs
for (let i = 0; i < count; i++) {
if (f.glyphIdFormat === 0) {
pairs_buf.writeUInt8(kernSorted[i][0], 2 * i);
pairs_buf.writeUInt8(kernSorted[i][1], 2 * i + 1);
} else {
pairs_buf.writeUInt16LE(kernSorted[i][0], 4 * i);
pairs_buf.writeUInt16LE(kernSorted[i][1], 4 * i + 2);
}
}
const values_buf = Buffer.alloc(count);
// Write kerning values
for (let i = 0; i < count; i++) {
values_buf.writeInt8(f.kernToFP(kernSorted[i][2]), i); // FP4.4
}
let buf = Buffer.concat([
subheader,
pairs_buf,
values_buf
]);
let buf_aligned = u.balign4(buf);
debug(`table format0 size = ${buf_aligned.length}`);
return buf_aligned;
}
collect_format3_data() {
const f = this.font;
const glyphs = u.sort_by(this.font.src.glyphs, g => f.glyph_id[g.code]);
// extract kerning pairs for each character;
// left kernings are kerning values based on left char (already there),
// right kernings are kerning values based on right char (extracted from left)
const left_kernings = {};
const right_kernings = {};
for (let g of glyphs) {
if (!g.kerning || !Object.keys(g.kerning).length) continue;
const paired = Object.keys(g.kerning);
left_kernings[g.code] = g.kerning;
for (let code of paired) {
right_kernings[code] = right_kernings[code] || {};
right_kernings[code][g.code] = g.kerning[code];
}
}
// input:
// - kernings, char => { hash: String, [char1]: Number, [char2]: Number, ... }
//
// returns:
// - array of [ char1, char2, ... ]
//
function build_classes(kernings) {
const classes = [];
for (let code of Object.keys(kernings)) {
// for each kerning table calculate unique value representing it;
// keys needs to be sorted for this (but we're using numeric keys, so
// sorting happens automatically and can't be changed)
const hash = JSON.stringify(kernings[code]);
classes[hash] = classes[hash] || [];
classes[hash].push(Number(code));
}
return Object.values(classes);
}
const left_classes = build_classes(left_kernings);
debug(`unique left classes: ${left_classes.length}`);
const right_classes = build_classes(right_kernings);
debug(`unique right classes: ${right_classes.length}`);
if (left_classes.length >= 255 || right_classes.length >= 255) {
debug('too many classes for format3 subtable');
return null;
}
function kern_class_mapping(classes) {
const arr = Array(f.last_id).fill(0);
classes.forEach((members, idx) => {
for (let code of members) {
arr[f.glyph_id[code]] = idx + 1;
}
});
return arr;
}
function kern_class_values() {
const arr = [];
for (let left_class of left_classes) {
for (let right_class of right_classes) {
let code1 = left_class[0];
let code2 = right_class[0];
arr.push(left_kernings[code1][code2] || 0);
}
}
return arr;
}
return {
left_classes: left_classes.length,
right_classes: right_classes.length,
left_mapping: kern_class_mapping(left_classes),
right_mapping: kern_class_mapping(right_classes),
values: kern_class_values()
};
}
create_format3_data() {
const f = this.font;
const {
left_classes,
right_classes,
left_mapping,
right_mapping,
values
} = this.collect_format3_data();
const subheader = Buffer.alloc(4);
subheader.writeUInt16LE(f.last_id);
subheader.writeUInt8(left_classes, 2);
subheader.writeUInt8(right_classes, 3);
let buf = Buffer.concat([
subheader,
Buffer.from(left_mapping),
Buffer.from(right_mapping),
Buffer.from(values.map(v => f.kernToFP(v)))
]);
let buf_aligned = u.balign4(buf);
debug(`table format3 size = ${buf_aligned.length}`);
return buf_aligned;
}
should_use_format3() {
if (!this.font.hasKerning()) return false;
const format0_data = this.create_format0_data();
const format3_data = this.create_format3_data();
if (format3_data && format3_data.length <= format0_data.length) return true;
if (this.font.opts.fast_kerning && format3_data) {
this.format3_forced = true;
return true;
}
return false;
}
toBin() {
if (!this.font.hasKerning()) return Buffer.alloc(0);
const format0_data = this.create_format0_data();
const format3_data = this.create_format3_data();
let header = Buffer.alloc(HEAD_LENGTH);
let data = format0_data;
header.writeUInt8(0, O_FORMAT);
/* eslint-disable no-console */
if (this.should_use_format3()) {
data = format3_data;
header.writeUInt8(3, O_FORMAT);
if (this.format3_forced) {
let diff = format3_data.length - format0_data.length;
console.log(`Forced faster kerning format (via classes). Size increase is ${diff} bytes.`);
}
} else if (this.font.opts.fast_kerning) {
console.log('Forced faster kerning format (via classes), but data exceeds it\'s limits. Continue use pairs.');
}
header.writeUInt32LE(header.length + data.length, O_SIZE);
header.write(this.label, O_LABEL);
return Buffer.concat([ header, data ]);
}
}
module.exports = Kern;

42
node_modules/lv_font_conv/lib/font/table_loca.js generated vendored Normal file
View File

@@ -0,0 +1,42 @@
'use strict';
const u = require('../utils');
const debug = require('debug')('font.table.loca');
const O_SIZE = 0;
const O_LABEL = O_SIZE + 4;
const O_COUNT = O_LABEL + 4;
const HEAD_LENGTH = O_COUNT + 4;
class Loca {
constructor(font) {
this.font = font;
this.label = 'loca';
}
toBin() {
const f = this.font;
const offsets = [ ...Array(f.last_id).keys() ].map(i => f.glyf.getOffset(i));
const buf = u.balign4(Buffer.concat([
Buffer.alloc(HEAD_LENGTH),
f.indexToLocFormat ? u.bFromA32(offsets) : u.bFromA16(offsets)
]));
buf.writeUInt32LE(buf.length, O_SIZE);
buf.write(this.label, O_LABEL);
buf.writeUInt32LE(f.last_id, O_COUNT);
debug(`table size = ${buf.length}`);
return buf;
}
}
module.exports = Loca;