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
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.