Skip to content

Add method to retrieve information about what characters in the original string were replaced by each glyph. #798

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
44 changes: 43 additions & 1 deletion src/bidi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
40 changes: 40 additions & 0 deletions src/font.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions test/bidi.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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] },
]);
});
});
});
34 changes: 34 additions & 0 deletions test/font.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
});
});
});
Binary file added test/fonts/NotoSans-Regular.ttf
Binary file not shown.