Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mark to base attachment positioning #4

Merged
merged 16 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ See [https://opentype.js.org/](https://opentype.js.org/) for a live demo.
* Support for composite glyphs (accented letters).
* Support for WOFF, OTF, TTF (both with TrueType `glyf` and PostScript `cff` outlines)
* Support for kerning (Using GPOS or the kern table).
* Support for Mark-to-Base Attachment Positioning.
* Support for ligatures.
* Support for TrueType font hinting.
* Support arabic text rendering (See issue #364 & PR #359 #361)
Expand Down
125 changes: 113 additions & 12 deletions src/bidi.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,18 @@
* the corresponding layout rules.
*/

import Tokenizer from './tokenizer.js';
import FeatureQuery from './features/featureQuery.js';
import Tokenizer, { ContextParams } from './tokenizer.js';
import FeatureQuery, { SubstitutionAction } from './features/featureQuery.js';
import arabicWordCheck from './features/arab/contextCheck/arabicWord.js';
import arabicSentenceCheck from './features/arab/contextCheck/arabicSentence.js';
import arabicPresentationForms from './features/arab/arabicPresentationForms.js';
import arabicRequiredLigatures from './features/arab/arabicRequiredLigatures.js';
import latinWordCheck from './features/latn/contextCheck/latinWord.js';
import latinLigature from './features/latn/latinLigatures.js';
import thaiWordCheck from './features/thai/contextCheck/thaiWord.js';
import thaiGlyphComposition from './features/thai/thaiGlyphComposition.js';
import thaiLigatures from './features/thai/thaiLigatures.js';
import thaiRequiredLigatures from './features/thai/thaiRequiredLigatures.js';
import unicodeVariationSequenceCheck from './features/unicode/contextCheck/variationSequenceCheck.js';
import unicodeVariationSequences from './features/unicode/variationSequences.js';
import applySubstitution from './features/applySubstitution.js';

/**
* Create Bidi. features
Expand Down Expand Up @@ -116,6 +114,7 @@ Bidi.prototype.applyFeatures = function (font, features) {
if (!font) throw new Error(
'No valid font was provided to apply features'
);
if (!this.font) this.font = font;
if (!this.query) this.query = new FeatureQuery(font);
for (let f = 0; f < features.length; f++) {
const feature = features[f];
Expand All @@ -134,6 +133,114 @@ Bidi.prototype.registerModifier = function (modifierId, condition, modifier) {
this.tokenizer.registerModifier(modifierId, condition, modifier);
};

function getContextParams(tokens, index) {
const context = tokens.map(token => token.activeState.value);
return new ContextParams(context, index || 0);
}

/**
* General method for processing GSUB tables with a specified algorithm:
* During text processing, a client applies a lookup to each glyph in the string before moving to the next lookup.
* A lookup is finished for a glyph after the client locates the target glyph or glyph context and performs a substitution, if specified.
*
* https://learn.microsoft.com/en-us/typography/opentype/spec/gsub#table-organization
*
* Use this algorithm instead of FeatureQuery.prototype.lookupFeature
*
* TODO: Support language option
* TODO: Consider moving this implementation to this.font.substitution (use layout.getFeaturesLookups)
*
* @param {string} script script name
* @param {array} features list of required features to process
*/
function applySubstitutions(script, features) {
const supportedFeatures = features.filter(feature => this.hasFeatureEnabled(script, feature));
const featuresLookups = this.query.getSubstitutionFeaturesLookups(supportedFeatures, script);
for (let idx = 0; idx < featuresLookups.length; idx++) {
const lookupTable = featuresLookups[idx];
const subtables = this.query.getLookupSubtables(lookupTable);
// Extract all thai words to apply the lookup feature per feature lookup table order
const ranges = this.tokenizer.getContextRanges(`${script}Word`); // use a context range name convention: latinWord, arabWord, thaiWord, etc.
for (let k = 0; k < ranges.length; k++) {
const range = ranges[k];
let tokens = this.tokenizer.getRangeTokens(range);
let contextParams = getContextParams(tokens);
for (let index = 0; index < contextParams.context.length; index++) {
contextParams.setCurrentIndex(index);
for (let s = 0; s < subtables.length; s++) {
const subtable = subtables[s];
const substType = this.query.getSubstitutionType(lookupTable, subtable);
const lookup = this.query.getLookupMethod(lookupTable, subtable);
let substitution;
switch (substType) {
case '11':
substitution = lookup(contextParams.current);
if (substitution) {
applySubstitution.call(this,
new SubstitutionAction({
id: 11, tag: lookupTable.feature, substitution
}),
tokens,
contextParams.index
);
}
break;
case '12':
substitution = lookup(contextParams.current);
if (substitution) {
applySubstitution.call(this,
new SubstitutionAction({
id: 12, tag: lookupTable.feature, substitution
}),
tokens,
contextParams.index
);
}
break;
case '63':
substitution = lookup(contextParams);
if (Array.isArray(substitution) && substitution.length) {
applySubstitution.call(this,
new SubstitutionAction({
id: 63, tag: lookupTable.feature, substitution
}),
tokens,
contextParams.index
);
}
break;
case '41':
substitution = lookup(contextParams);
if (substitution) {
applySubstitution.call(this,
new SubstitutionAction({
id: 41, tag: lookupTable.feature, substitution
}),
tokens,
contextParams.index
);
}
break;
case '21':
substitution = lookup(contextParams.current);
if (Array.isArray(substitution) && substitution.length) {
applySubstitution.call(this,
new SubstitutionAction({
id: 21, tag: lookupTable.feature, substitution
}),
tokens,
range.startIndex + index
);
}
break;
}
}
contextParams = getContextParams(tokens, index);
}
}
}
}

/**
* Check if 'glyphIndex' is registered
*/
Expand Down Expand Up @@ -199,13 +306,7 @@ function applyUnicodeVariationSequences() {
*/
function applyThaiFeatures() {
checkGlyphIndexStatus.call(this);
const ranges = this.tokenizer.getContextRanges('thaiWord');
for(let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (this.hasFeatureEnabled('thai', 'liga')) thaiLigatures.call(this, range);
if (this.hasFeatureEnabled('thai', 'rlig')) thaiRequiredLigatures.call(this, range);
if (this.hasFeatureEnabled('thai', 'ccmp')) thaiGlyphComposition.call(this, range);
}
applySubstitutions.call(this, 'thai', ['liga', 'rlig', 'ccmp']);
}

/**
Expand Down
31 changes: 29 additions & 2 deletions src/features/applySubstitution.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SubstitutionAction } from './featureQuery.js';
import { Token } from '../tokenizer.js';

/**
* Apply single substitution format 1
Expand Down Expand Up @@ -50,14 +51,40 @@ function ligatureSubstitutionFormat1(action, tokens, index) {
}
}

/**
* Apply multiple substitution format 1
* @param {Array} substitutions substitutions
* @param {any} tokens a list of tokens
* @param {number} index token index
*/
function multiSubstitutionFormat1(action, tokens, index) {
if (this.font && this.tokenizer) {
const newTokensList = [];
const substitution = action.substitution;
for (let i = 0; i < substitution.length; i++) {
const substitutionGlyphIndex = substitution[i];
const glyph = this.font.glyphs.get(substitutionGlyphIndex);
const token = new Token(String.fromCharCode(parseInt(glyph.unicode)));
token.setState('glyphIndex', substitutionGlyphIndex);
newTokensList.push(token);
}

// Replace single range (glyph) index with multiple glyphs
if (newTokensList.length) {
this.tokenizer.replaceRange(index, 1, newTokensList);
}
}
}

/**
* Supported substitutions
*/
const SUBSTITUTIONS = {
11: singleSubstitutionFormat1,
12: singleSubstitutionFormat2,
63: chainingSubstitutionFormat3,
41: ligatureSubstitutionFormat1
41: ligatureSubstitutionFormat1,
21: multiSubstitutionFormat1
};

/**
Expand All @@ -68,7 +95,7 @@ const SUBSTITUTIONS = {
*/
function applySubstitution(action, tokens, index) {
if (action instanceof SubstitutionAction && SUBSTITUTIONS[action.id]) {
SUBSTITUTIONS[action.id](action, tokens, index);
SUBSTITUTIONS[action.id].call(this, action, tokens, index);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/arab/arabicPresentationForms.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function arabicPresentationForms(range) {
for(let j = 0; j < substitutions.length; j++) {
const action = substitutions[j];
if (action instanceof SubstitutionAction) {
applySubstitution(action, tokens, j);
applySubstitution.call(this, action, tokens, j);
contextParams.context[j] = action.substitution;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/arab/arabicRequiredLigatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function arabicRequiredLigatures(range) {
if (substitutions.length) {
for(let i = 0; i < substitutions.length; i++) {
const action = substitutions[i];
applySubstitution(action, tokens, index);
applySubstitution.call(this, action, tokens, index);
}
contextParams = getContextParams(tokens);
}
Expand Down
17 changes: 17 additions & 0 deletions src/features/featureQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ FeatureQuery.prototype.getLookupMethod = function(lookupTable, subtable) {
/**
* Lookup a feature using a query parameters
* @param {FQuery} query feature query
* @deprecated Use bidi.applySubstitutions(...)
*/
FeatureQuery.prototype.lookupFeature = function (query) {
let contextParams = query.contextParams;
Expand Down Expand Up @@ -444,6 +445,22 @@ FeatureQuery.prototype.lookupFeature = function (query) {
return substitutions.length ? substitutions : null;
};

/**
* Assembling features into ordered lookup list (wrapper)
* Assemble all features (including any required feature) for the glyph run’s language system.
* Assemble all lookups in these features, in LookupList order, removing any duplicates.
*
* https://learn.microsoft.com/en-us/typography/opentype/otspec191alpha/chapter2#lookup-table
*
* @param {string[]} list of requested features
* @param {string} script
* @param {string} language
* @return {Object[]} ordered lookup processing list
*/
FeatureQuery.prototype.getSubstitutionFeaturesLookups = function(features, script, language) {
return this.font.substitution.getFeaturesLookups(features, script, language);
};

/**
* Checks if a font supports a specific features
* @param {FQuery} query feature query object
Expand Down
2 changes: 1 addition & 1 deletion src/features/latn/latinLigatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function latinLigature(range) {
if (substitutions.length) {
for(let i = 0; i < substitutions.length; i++) {
const action = substitutions[i];
applySubstitution(action, tokens, index);
applySubstitution.call(this, action, tokens, index);
}
contextParams = getContextParams(tokens);
}
Expand Down
2 changes: 2 additions & 0 deletions src/features/positioning/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './kern';
export * from './mark';
20 changes: 20 additions & 0 deletions src/features/positioning/kern.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Apply kerning positioning advance glyphs advance
*/

function kern(lookupTable, glyphs) {
const coords = [];
for (let i = 0; i < glyphs.length; i += 1) {
const glyph = glyphs[i];
coords[i] = { xAdvance: 0, yAdvance: 0 };
if (i > 0) {
coords[i] = {
xAdvance: this.position.getKerningValue([lookupTable], glyphs[i - 1].index, glyph.index),
yAdvance: 0
};
}
}
return coords;
}

export { kern };
25 changes: 25 additions & 0 deletions src/features/positioning/mark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Apply MarkToBase positioning advance glyphs advance
*/

function mark(lookupTable, glyphs) {
const coords = [];
for (let i = 0; i < glyphs.length; i += 1) {
const glyph = glyphs[i];
coords[i] = { xAdvance: 0, yAdvance: 0 };
if (i > 0) {
const coordinatedPair = this.position.getMarkToBaseAttachment([lookupTable], glyphs[i - 1].index, glyph.index);
if (coordinatedPair) {
const { attachmentMarkPoint, baseMarkPoint } = coordinatedPair;
// Base mark's advanceWidth must be ignored to have a proper positiong for the attachment mark
coords[i] = {
xAdvance: baseMarkPoint.xCoordinate - attachmentMarkPoint.xCoordinate - glyphs[i - 1].advanceWidth,
yAdvance: baseMarkPoint.yCoordinate - attachmentMarkPoint.yCoordinate
};
}
}
}
return coords;
}

export { mark };
41 changes: 0 additions & 41 deletions src/features/thai/thaiGlyphComposition.js

This file was deleted.

Loading
Loading