diff --git a/src/bidi.mjs b/src/bidi.mjs index aefb425e..fe7acef6 100644 --- a/src/bidi.mjs +++ b/src/bidi.mjs @@ -294,7 +294,7 @@ Bidi.prototype.getBidiText = function (text) { /** * Get the current state index of each token - * @param {text} text an input text + * @param {string} text an input text */ Bidi.prototype.getTextGlyphs = function (text) { this.processText(text); @@ -308,4 +308,46 @@ Bidi.prototype.getTextGlyphs = function (text) { return indexes; }; +/** + * Gets an array of glyph indices, as well as mapping information for + * which characters in the original text each glyph replaced. + * @param {string} text an input text + * @return {Array} example: + * Input: "fla" + * Output: `[ { index: 1655, replaced: [0, 1] }, { index: 68, replaced: [2] } ]` + */ +Bidi.prototype.getTextGlyphMapping = function (text) { + this.processText(text); + let mapping = []; + let lastIndex = null; + let replaced = []; + for (let i = 0; i < this.tokenizer.tokens.length; i++) { + const token = this.tokenizer.tokens[i]; + if (token.state.deleted) { + replaced.push(i); + continue; + } + if (lastIndex != null) { + mapping.push({ + index: lastIndex, + replaced, + }); + lastIndex = null; + replaced = []; + } + const index = token.activeState.value; + lastIndex = Array.isArray(index) ? index[0] : index; + replaced.push(i); + } + if (lastIndex != null) { + mapping.push({ + index: lastIndex, + replaced, + }); + lastIndex = null; + replaced = []; + } + return mapping; +}; + export default Bidi; diff --git a/src/font.mjs b/src/font.mjs index b87cf6cd..6765c0f9 100644 --- a/src/font.mjs +++ b/src/font.mjs @@ -253,6 +253,46 @@ Font.prototype.stringToGlyphIndexes = function(s, options) { return bidi.getTextGlyphs(s); }; +/** + * Convert the given text to an array of Glyphs and associated mapping of + * which characters in the original text were replaced by each Glyph index. + * @param {string} + * @param {GlyphRenderOptions} [options] + * @return {Array} example: + * Input: "fla" + * Output: `[ { glyph: opentype.Glyph, replaced: [0, 1] }, { glyph: opentype.Glyph, replaced: [2] } ]` + */ +Font.prototype.stringToGlyphMapping = function(s, options) { + const bidi = new Bidi(); + + // Create and register 'glyphIndex' state modifier + const charToGlyphIndexMod = token => this.charToGlyphIndex(token.char); + bidi.registerModifier('glyphIndex', null, charToGlyphIndexMod); + + // roll-back to default features + let features = options ? + this.updateFeatures(options.features) : + this.defaultRenderOptions.features; + + bidi.applyFeatures(this, features); + + const indexMapping = bidi.getTextGlyphMapping(s); + + let length = indexMapping.length; + + // convert glyph indices to glyph objects + const glyphMapping = new Array(length); + const notdef = this.glyphs.get(0); + for (let i = 0; i < length; i += 1) { + glyphMapping[i] = { + glyph: this.glyphs.get(indexMapping[i].index) || notdef, + replaced: indexMapping[i].replaced, + }; + } + + return glyphMapping; +}; + /** * Convert the given text to a list of Glyph objects. * Note that there is no strict one-to-one mapping between characters and diff --git a/test/bidi.spec.mjs b/test/bidi.spec.mjs index 08c6ef49..a958798e 100644 --- a/test/bidi.spec.mjs +++ b/test/bidi.spec.mjs @@ -177,4 +177,45 @@ describe('bidi.mjs', function() { }); }); }); + + describe('glyph mapping', function () { + let notoSansFont; + let bidi; + + const features = [{ + script: 'latn', + tags: ['liga', 'rlig'] + }]; + + beforeEach(()=> { + notoSansFont = loadSync('./test/fonts/NotoSans-Regular.ttf'); + }); + + it('maps multiple glyphs to multiple characters in the original text source', () => { + bidi = new Bidi(); + bidi.applyFeatures(notoSansFont, features); + + bidi.registerModifier( + 'glyphIndex', null, token => notoSansFont.charToGlyphIndex(token.char) + ); + + let glyphMap = bidi.getTextGlyphMapping('fla'); + assert.deepEqual(glyphMap, [ + { index: 1655, replaced: [0, 1] }, + { index: 68, replaced: [2] } + ]); + + glyphMap = bidi.getTextGlyphMapping('flafl'); + assert.deepEqual(glyphMap, [ + { index: 1655, replaced: [0, 1] }, + { index: 68, replaced: [2] }, + { index: 1655, replaced: [3, 4] }, + ]); + + glyphMap = bidi.getTextGlyphMapping('ff'); + assert.deepEqual(glyphMap, [ + { index: 1653, replaced: [0, 1] }, + ]); + }); + }); }); diff --git a/test/font.spec.mjs b/test/font.spec.mjs index 05e00f08..e479fe8e 100644 --- a/test/font.spec.mjs +++ b/test/font.spec.mjs @@ -257,4 +257,38 @@ describe('glyphset.mjs', function() { assert.deepEqual(fillLogs, expectedColors); }); }); + + describe('glyph mapping', function() { + let notoSansFont; + + beforeEach(()=> { + notoSansFont = loadSync('./test/fonts/NotoSans-Regular.ttf'); + }); + + it('maps multiple glyphs to multiple characters in the original text source', () => { + let mapping = notoSansFont.stringToGlyphMapping('ff'); + assert.equal(mapping[0].glyph.index, 1653); // ff + assert.deepEqual(mapping[0].replaced, [0, 1]); + + mapping = notoSansFont.stringToGlyphMapping('ffi'); + assert.equal(mapping[0].glyph.index, 1656); // ffi + assert.deepEqual(mapping[0].replaced, [0, 1, 2]); + + mapping = notoSansFont.stringToGlyphMapping('ffiff'); + assert.equal(mapping[0].glyph.index, 1656); // ffi + assert.deepEqual(mapping[0].replaced, [0, 1, 2]); + assert.equal(mapping[1].glyph.index, 1653); // ff + assert.deepEqual(mapping[1].replaced, [3, 4]); + + mapping = notoSansFont.stringToGlyphMapping('fffiffif'); + assert.equal(mapping[0].glyph.index, 1653); // ff + assert.deepEqual(mapping[0].replaced, [0, 1]); + assert.equal(mapping[1].glyph.index, 1654); // fi + assert.deepEqual(mapping[1].replaced, [2, 3]); + assert.equal(mapping[2].glyph.index, 1656); // ffi + assert.deepEqual(mapping[2].replaced, [4, 5, 6]); + assert.equal(mapping[3].glyph.index, 73); // f + assert.deepEqual(mapping[3].replaced, [7]); + }); + }); }); \ No newline at end of file diff --git a/test/fonts/NotoSans-Regular.ttf b/test/fonts/NotoSans-Regular.ttf new file mode 100644 index 00000000..fa4cff50 Binary files /dev/null and b/test/fonts/NotoSans-Regular.ttf differ