Skip to content

Commit 8d2c259

Browse files
committed
update samsa-core.js
1 parent 980f38f commit 8d2c259

1 file changed

Lines changed: 100 additions & 95 deletions

File tree

src/samsa-core.js

Lines changed: 100 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,8 +1214,7 @@ function coverageIndexForGlyph (coverage, g) {
12141214
coverageIndex = coverage.glyphArray.indexOf(g);
12151215
}
12161216
else if (coverage.format === 2) {
1217-
for (let ra=0; ra<coverage.glyphRanges.length; ra++) {
1218-
const range = coverage.glyphRanges[ra];
1217+
for (const range of coverage.glyphRanges) {
12191218
if (g >= range.startGlyphID && g <= range.endGlyphID) {
12201219
coverageIndex = range.startCoverageIndex + g - range.startGlyphID;
12211220
break;
@@ -2073,29 +2072,39 @@ class SamsaBuffer extends DataView {
20732072
return this.decodeString(this.u8);
20742073
}
20752074

2076-
decodeGlyph(numBytes, options={}) {
2077-
2075+
decodeGlyph(byteLength, options={}) {
2076+
20782077
const metrics = options.metrics ?? [0, 0, 0, 0];
2079-
const glyph = numBytes ? new SamsaGlyph(this.decode(FORMATS.GlyphHeader)) : new SamsaGlyph();
2078+
const glyphOffset = this.tell();
2079+
const glyph = byteLength ? new SamsaGlyph(this.decode(FORMATS.GlyphHeader)) : new SamsaGlyph();
20802080
if (options.id)
20812081
glyph.id = options.id;
20822082

2083+
// set offset & length
2084+
glyph.offset = glyphOffset;
2085+
glyph.length = byteLength;
2086+
20832087
// set metrics from the font’s hmtx
20842088
glyph.font = options.font;
20852089
if (glyph.font.hmtx) {
20862090
metrics[1] = glyph.font.hmtx[glyph.id];
20872091
}
20882092

20892093
// empty glyph
2090-
if (numBytes === 0) {
2094+
if (byteLength === 0) {
20912095
}
20922096

20932097
// simple glyph
20942098
else if (glyph.numberOfContours >= 0) { // note that the spec allows for numberOfContours to be 0, in which case we have a zero-contour glyph but with the possibility of instructions to move phantom points
20952099

20962100
glyph.endPts = this.u16_arrayOfLength(glyph.numberOfContours); // end points of each contour
20972101
glyph.numPoints = glyph.endPts[glyph.numberOfContours -1] + 1;
2098-
this.seekr(glyph.instructionLength = this.u16); // skip over instructions
2102+
2103+
// get instructions into a SamsaBuffer
2104+
if (glyph.instructionLength = this.u16) {
2105+
glyph.instructions = new SamsaBuffer(this.buffer, this.byteOffset + this.tell(), glyph.instructionLength);
2106+
}
2107+
this.seekr(glyph.instructionLength);
20992108

21002109
// flags
21012110
const flags = [];
@@ -2217,13 +2226,11 @@ class SamsaBuffer extends DataView {
22172226
// - maximum size of the compiled glyph in bytes (without padding) if options.getMaxCompiledSize
22182227

22192228
// set default options
2220-
if (options.compression === undefined)
2221-
options.compression = 1; // standard TrueType compression
2222-
if (options.bbox === undefined)
2223-
options.bbox = true; // recalculate bounding box
2229+
options.compression ??= 1; // standard TrueType compression
2230+
options.bbox ??= true; // recalculate bounding box
22242231

22252232
if (options.getMaxCompiledSize) {
2226-
if (options.compression == 2) {
2233+
if (options.compression === 2) {
22272234
if (glyph.numberOfContours > 0)
22282235
return
22292236
else if (glyph.numberOfContours < 0)
@@ -2424,25 +2431,16 @@ class SamsaBuffer extends DataView {
24242431
}
24252432

24262433
else {
2427-
24282434
// options.compression != 2
2429-
2430-
// new header with bbox
2431-
this.encode(FORMATS.GlyphHeader, glyph);
2432-
2433-
// endpoints
2434-
glyph.endPts.forEach(endPt => {
2435-
this.u16 = endPt;
2436-
});
2437-
2438-
// instructions (none for now)
2439-
this.u16 = instructionLength;
2435+
this.encode(FORMATS.GlyphHeader, glyph); // new header with bbox
2436+
this.u16_array = glyph.endPts; // write endpoints array
2437+
this.u16 = instructionLength; // instructions (none for now)
24402438
this.seekr(instructionLength);
24412439

24422440
// write glyph points
24432441

24442442
// compression: none
2445-
if (options.compression == 0) {
2443+
if (options.compression === 0) {
24462444

24472445
// write uncompressed glyph points (faster in memory and for SSD disks)
24482446
let cx=0;
@@ -2489,11 +2487,10 @@ class SamsaBuffer extends DataView {
24892487
cy = y;
24902488
}
24912489
}
2492-
24932490
}
24942491

24952492
// compression: standard TrueType compression
2496-
else if (options.compression == 1) {
2493+
else if (options.compression === 1) {
24972494

24982495
// write compressed glyph points (slower)
24992496
let cx=0, cy=0;
@@ -2642,38 +2639,40 @@ class SamsaBuffer extends DataView {
26422639
}
26432640

26442641
// encodeInstance is how we export a static font!
2645-
encodeInstance(instance, options={format: "truetype"}) {
2646-
// options.format: "truetype" | "cff2"
2647-
// options.checkSums: true | false
2648-
// TODO: instantiate MVAR, cvar, GSUB etc.
2642+
// - TODO: instantiate MVAR, cvar, GSUB etc.
2643+
encodeInstance(instance, options={}) {
2644+
options.format ??= "truetype";
2645+
options.checkSums ??= true;
2646+
options.glyphCompression ??= true;
26492647

2650-
const startTime = performance.now();
26512648
const font = instance.font;
26522649
const numGlyphs = font.maxp.numGlyphs;
2653-
const tables = font.tableList.filter(table => !["fvar", "gvar", "avar", "cvar", "HVAR", "VVAR", "MVAR", "STAT"].includes(table.tag)); // remove variable-specific tables
2654-
const tableDirectory = {};
2655-
const locas = [0];
2650+
const tableListFiltered = font.tableList.filter(table => !["fvar", "gvar", "avar", "cvar", "HVAR", "VVAR", "MVAR", "STAT"].includes(table.tag)); // remove variable-specific tables
2651+
const tableListNew = [];
2652+
const tableDirectory = {}; // the new table directory
2653+
const locas = [0]; // TODO: use a Uint32Array and bit-shift to divide by 2 for max performance
26562654
const hMetrics = [];
2657-
const outputBufU8 = new Uint8Array(this.buffer); // we can use outputBufU8[outputBuf.p] to write bytes to the current buffer position
2655+
const outputBufU8 = new Uint8Array(this.buffer, this.byteOffset); // we can use outputBufU8[outputBuf.p] to write bytes to the current buffer position
26582656
let indexToLocFormat;
26592657
let checkSumTotal = 0;
26602658

26612659
// skip the header, then write each table
2662-
this.seek(12 + tables.length * 16);
2663-
tables.forEach(table => {
2664-
2665-
table.offset = this.tell();
2666-
table.checkSum = 0;
2667-
tableDirectory[table.tag] = table;
2660+
// - "glyf" must be written first, as it creates arrays used by "loca" and "hmtx"
2661+
// - fortunately this happens naturally due to ordering by table tag
2662+
// - this table ordering is contrary to some optimizers, such as the old CACHETT.EXE
2663+
this.seek(12 + tableListFiltered.length * 16);
2664+
tableListFiltered.forEach(table => {
2665+
2666+
const tableNew = { tag: table.tag, checkSum: 0, length: 0, offset: this.tell() };
26682667
switch (table.tag) {
26692668
case "glyf": {
26702669
for (let g = 0; g < numGlyphs; g++) {
26712670
const glyph = font.glyphs[g] ?? font.loadGlyphById(g);
26722671
const iglyph = glyph.instantiate(instance); // load & instantiate glyph, no decomposition
2673-
this.encodeGlyph(iglyph, {bbox: true});
2672+
this.encodeGlyph(iglyph, { bbox: true, compression: +options.glyphCompression }); // "+" converts false -> 0, true -> 1"
26742673
hMetrics.push([Math.round(iglyph.points[iglyph.numPoints+1][0]), iglyph.xMin]); // advanceWidth, leftSideBearing
26752674
this.padToModulo(2);
2676-
locas.push(this.tell() - table.offset); // since locas is initialized as [0], locas.length ends up as numGlyphs+1
2675+
locas.push(this.tell() - tableNew.offset); // since locas is initialized as [0], locas.length ends up as numGlyphs+1
26772676
}
26782677
break;
26792678
}
@@ -2687,7 +2686,7 @@ class SamsaBuffer extends DataView {
26872686
case "hmtx": {
26882687
for (const [advanceWidth, lsb] of hMetrics) {
26892688
this.u16 = Math.max(0, advanceWidth); // negative values are not allowed
2690-
this.i16 = lsb; // TODO: left side bearing
2689+
this.i16 = lsb; // TODO: exploit advanceWidth compression where hhea.numberOfHMetrics < numGlyphs
26912690
}
26922691
break;
26932692
}
@@ -2699,14 +2698,19 @@ class SamsaBuffer extends DataView {
26992698
break;
27002699
}
27012700
}
2702-
table.length = this.tell() - table.offset;
2701+
2702+
// table.length = this.tell() - table.offset;
2703+
tableNew.length = this.tell() - tableNew.offset;
2704+
tableListNew.push(tableNew);
2705+
tableDirectory[table.tag] = tableNew;
27032706
this.padToModulo(4);
27042707
});
27052708

27062709
// store the total length... now we will fix the header and table directory
27072710
const finalLength = this.tell();
27082711

27092712
// fixups
2713+
// - these write to the new font data just written to memory, and do not affect the original loaded font data
27102714
this.seek(tableDirectory.head.offset + 50); // fix head.indexToLocFormat to the value actually used, rather the one read from the input
27112715
this.u16 = indexToLocFormat; // either 0 or 1
27122716
this.seek(tableDirectory.head.offset + 8); // fix head.checkSumAdjustment to zero so we can correclty checksum
@@ -2716,7 +2720,7 @@ class SamsaBuffer extends DataView {
27162720

27172721
// table checkSums
27182722
if (options.checkSums) {
2719-
for (const table of tables) {
2723+
for (const table of tableListNew) {
27202724
checkSumTotal += table.checkSum = this.checkSum(table.offset, table.length);
27212725
checkSumTotal &= 0xffffffff;
27222726
}
@@ -2725,24 +2729,21 @@ class SamsaBuffer extends DataView {
27252729
// write final header and table directory
27262730
this.seek(0);
27272731
this.u32 = font.header.sfntVersion;
2728-
this.u16 = tables.length;
2729-
this.u16_array = font.binarySearchParams(tables.length); // write 3 U16s for the binary search params
2730-
tables
2732+
this.u16 = tableListNew.length;
2733+
this.u16_array = font.binarySearchParams(tableListNew.length); // write 3 U16s for the binary search params
2734+
tableListNew
27312735
.sort((a,b) => compareString(a.tag, b.tag)) // sort by tag
27322736
.forEach(table => this.u32_array = this.tableDirectoryEntry(table)); // write 4 U32s for each table directory entry
27332737

27342738
// final fixups
27352739
if (options.checkSums) {
2736-
checkSumTotal += this.checkSum(0, 12 + 16 * tables.length); // add header checkSum
2740+
checkSumTotal += this.checkSum(0, 12 + 16 * tableListNew.length); // add header checkSum
27372741
checkSumTotal &= 0xffffffff;
27382742
this.seek(tableDirectory.head.offset + 8); // write the final value into head.checkSumAdjustment
27392743
this.u32 = ((0xB1B0AFBA - checkSumTotal) + 0x100000000) % 0xffffffff;
27402744
}
2741-
2742-
const endTime = performance.now();
2743-
console.log("Font encoding time: " + (endTime - startTime) + " ms");
2744-
2745-
return finalLength; // now the buffer "this" contains the binary font, we return the length to the client as the buffer is larger than the font
2745+
this.seek(finalLength);
2746+
return finalLength; // now the buffer "this" contains the binary font, we return the length to the client (finalLength <= this.byteLength)
27462747
}
27472748

27482749
decodeItemVariationStore() {
@@ -2924,7 +2925,7 @@ class SamsaBuffer extends DataView {
29242925
if (numPoints < 0x80)
29252926
this.u8 = numPoints; // set length byte
29262927
else
2927-
this.u16 = numPoints | (GVAR_POINTS_ARE_WORDS << 8); // set length byte
2928+
this.u16 = numPoints | (GVAR_POINTS_ARE_WORDS << 8); // set length bit
29282929

29292930
// this method avoids the need to create arrays of runs
29302931
const u8PackedTell = this.byteOffset + this.tell();
@@ -4424,6 +4425,38 @@ SamsaFont.prototype.instance = function (axisSettings={}) {
44244425
return new SamsaInstance(this, axisSettings);
44254426
}
44264427

4428+
// convenience functions
4429+
SamsaFont.prototype.axes = function () {
4430+
return this.fvar ? this.fvar.axes : [];
4431+
}
4432+
4433+
SamsaFont.prototype.instances = function () {
4434+
return this.fvar ? this.fvar.instances : [];
4435+
}
4436+
4437+
SamsaFont.prototype.fvsFromCoordinates = function (coordinates) { // note that coordinates here are not normalized, so directly from the user or the fvar table
4438+
const fvs = {};
4439+
if (this.fvar) {
4440+
this.fvar.axes.forEach((axis,a) => {
4441+
if (coordinates === undefined)
4442+
fvs[axis.axisTag] = axis.defaultValue; // set coordinates == undefined to get a default fvs (of course you can use an empty object for this)
4443+
else
4444+
fvs[axis.axisTag] = coordinates[a];
4445+
});
4446+
}
4447+
return fvs;
4448+
}
4449+
4450+
SamsaFont.prototype.coordinatesFromFvs = function (axisSettings) {
4451+
const coordinates = [];
4452+
if (this.fvar) {
4453+
this.fvar.axes.forEach((axis,a) => {
4454+
coordinates[a] = axisSettings[axis.axisTag] ?? axis.defaultValue;
4455+
});
4456+
}
4457+
return coordinates;
4458+
}
4459+
44274460

44284461
//-------------------------------------------------------------------------------
44294462
// SamsaInstance
@@ -4434,6 +4467,8 @@ function SamsaInstance(font, axisSettings={}, options={}) {
44344467
this.font = font;
44354468
if (options.name)
44364469
this.name = options.name;
4470+
if (options.ppem)
4471+
this.ppem = options.ppem;
44374472
const {avar, gvar} = font; // destructure table data objects
44384473
this.axisSettings = {...axisSettings};
44394474
this.coordinates = font.coordinatesFromFvs(axisSettings); // the coordinates of the instance in user space
@@ -4582,40 +4617,6 @@ function SamsaInstance(font, axisSettings={}, options={}) {
45824617
}
45834618
}
45844619

4585-
4586-
// convenience functions
4587-
SamsaFont.prototype.axes = function () {
4588-
return this.fvar ? this.fvar.axes : [];
4589-
}
4590-
4591-
SamsaFont.prototype.instances = function () {
4592-
return this.fvar ? this.fvar.instances : [];
4593-
}
4594-
4595-
SamsaFont.prototype.fvsFromCoordinates = function (coordinates) { // note that coordinates here are not normalized, so directly from the user or the fvar table
4596-
const fvs = {};
4597-
if (this.fvar) {
4598-
this.fvar.axes.forEach((axis,a) => {
4599-
if (coordinates === undefined)
4600-
fvs[axis.axisTag] = axis.defaultValue; // set coordinates == undefined to get a default fvs (of course you can use an empty object for this)
4601-
else
4602-
fvs[axis.axisTag] = coordinates[a];
4603-
});
4604-
}
4605-
return fvs;
4606-
}
4607-
4608-
SamsaFont.prototype.coordinatesFromFvs = function (axisSettings) {
4609-
const coordinates = [];
4610-
if (this.fvar) {
4611-
this.fvar.axes.forEach((axis,a) => {
4612-
coordinates[a] = axisSettings[axis.axisTag] ?? axis.defaultValue;
4613-
});
4614-
}
4615-
return coordinates;
4616-
}
4617-
4618-
46194620
// SamsaInstance.glyphAdvance() - return the advance of a glyph
46204621
// - we need this method in SamsaInstance, not SamsaGlyph, because SamsaGlyph might not be loaded (and we don’t need to load it, because we have hmtx and HVAR)
46214622
// - if we have a variable font and HVAR is not present, we must load the glyph in order to know its variable advance
@@ -4943,7 +4944,7 @@ SamsaInstance.prototype.glyphRunGSUB = function (inputRun, options={}) {
49434944

49444945
// get coverage for this lookup
49454946
if (subtable.coverageOffset) {
4946-
buf.seek(subtableOffset + coverageOffset);
4947+
buf.seek(subtableOffset + subtable.coverageOffset);
49474948
subtable.coverage = buf.decodeCoverage();
49484949
}
49494950
}
@@ -5597,9 +5598,13 @@ function SamsaGlyph (init={}) {
55975598
this.points = init.points || [];
55985599
this.components = init.components || [];
55995600
this.endPts = init.endPts || [];
5600-
this.tvts = init.tvts ? font.gvar.buffer.decodeTvts(this) : undefined; // init.tvts is boolean
5601-
this.curveOrder = 2;
5602-
//this.curveOrder = init.curveOrder || (this.font ? this.font.curveOrder : undefined);
5601+
this.tvts = init.tvts ? font.gvar.buffer.decodeTvts(this) : undefined; // init.tvts is boolean // TODO: this doesn’t work, move it to an options parameter?
5602+
this.curveOrder = init.curveOrder || 2;
5603+
5604+
this.xMin = init.xMin || 0;
5605+
this.yMin = init.yMin || 0;
5606+
this.xMax = init.xMax || 0;
5607+
this.yMax = init.yMax || 0;
56035608
}
56045609

56055610

0 commit comments

Comments
 (0)