Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/opentype/GPOSProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ export default class GPOSProcessor extends OTProcessor {
return { x, y };
}

applyFeatures(userFeatures, glyphs, advances) {
super.applyFeatures(userFeatures, glyphs, advances);
applyFeatures(featureTags, glyphs, advances, userFeatures) {
super.applyFeatures(featureTags, glyphs, advances, userFeatures);

for (var i = 0; i < this.glyphs.length; i++) {
this.fixCursiveAttachment(i);
Expand Down
13 changes: 11 additions & 2 deletions src/opentype/GSUBProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,17 @@ export default class GSUBProcessor extends OTProcessor {
case 3: { // Alternate Substitution
let index = this.coverageIndex(table.coverage);
if (index !== -1) {
let USER_INDEX = 0; // TODO
this.glyphIterator.cur.id = table.alternateSet.get(index)[USER_INDEX];
// Honour the user's 1-based alternate index for the current feature
// (e.g. `nalt: 2` selects the second alternate). Fall back to the
// first alternate when no value was given or the value isn't a
// positive integer.
let alt = this.userFeatures && this.userFeatures[this.currentFeature];
let userIndex = Number.isInteger(alt) && alt > 0 ? alt - 1 : 0;
let alternates = table.alternateSet.get(index);
if (userIndex >= alternates.length) {
userIndex = 0;
}
this.glyphIterator.cur.id = alternates[userIndex];
return true;
}

Expand Down
14 changes: 10 additions & 4 deletions src/opentype/OTLayoutEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ export default class OTLayoutEngine {
this.plan = new ShapingPlan(this.font, script, glyphRun.direction);
this.shaper.plan(this.plan, this.glyphInfos, glyphRun.features);

// Assign chosen features to output glyph run
for (let key in this.plan.allFeatures) {
glyphRun.features[key] = true;
}
// Build the output features object: every shaper-chosen feature marked
// `true`, then overlay the user's original values so alternateNumber
// selections (e.g. `nalt: 2` for Type 3 lookups) survive instead of being
// clobbered to `true`. The caller's input object is left untouched.
glyphRun.features = {
...Object.fromEntries(
Object.keys(this.plan.allFeatures).map(k => [k, true])
),
...glyphRun.features
};
}

substitute(glyphRun) {
Expand Down
5 changes: 3 additions & 2 deletions src/opentype/OTProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,9 @@ export default class OTProcessor {
});
}

applyFeatures(userFeatures, glyphs, advances) {
let lookups = this.lookupsForFeatures(userFeatures);
applyFeatures(featureTags, glyphs, advances, userFeatures) {
this.userFeatures = userFeatures;
let lookups = this.lookupsForFeatures(featureTags);
this.applyLookups(lookups, glyphs, advances);
}

Expand Down
8 changes: 7 additions & 1 deletion src/opentype/ShapingPlan.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export default class ShapingPlan {
this.stages = [];
this.globalFeatures = {};
this.allFeatures = {};
// Snapshot of the user's original features object — preserved for
// Type 3 (Alternate Substitution) lookups that need the per-feature
// alternate index (1-based; e.g. `nalt: 2` selects the second
// alternate). Set in setFeatureOverrides.
this.userFeatures = null;
}

/**
Expand Down Expand Up @@ -76,6 +81,7 @@ export default class ShapingPlan {
if (Array.isArray(features)) {
this.add(features);
} else if (typeof features === 'object') {
this.userFeatures = features;
for (let tag in features) {
if (features[tag]) {
this.add(tag);
Expand Down Expand Up @@ -111,7 +117,7 @@ export default class ShapingPlan {
}

} else if (stage.length > 0) {
processor.applyFeatures(stage, glyphs, positions);
processor.applyFeatures(stage, glyphs, positions, this.userFeatures);
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions test/shaping.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,4 +582,23 @@ describe('shaping', function () {
test('SHBALI-2/12', 'NotoSans/NotoSansBalinese-Regular.ttf', "ᬓ᭄ᭅᬸ", '23+2275|162+0|60@0,-1000+0');
});
});

describe('alternate substitution (GSUB Type 3) honours user-supplied alternateNumber', function () {
// FiraSans's aalt lookup carries multiple alternates per coverage entry.
// For 'A' (glyph 3) the alternate set is [1078 'ordfeminine', 764 'a.sc'].
// Without honouring the user-supplied alternateNumber, every aalt request
// resolves to the first entry — matching HarfBuzz's `aalt=2` requires
// selecting the second.
let font = fontkit.openSync(new URL('data/FiraSans/FiraSans-Regular.ttf', import.meta.url));

it('selects the first alternate when aalt is `true` (or 1)', function () {
let { glyphs } = font.layout('A', { aalt: true });
assert.deepEqual(glyphs.map(g => g.id), [1078]);
});

it('selects the second alternate when aalt is 2', function () {
let { glyphs } = font.layout('A', { aalt: 2 });
assert.deepEqual(glyphs.map(g => g.id), [764]);
});
});
});