|
|
import {binarySearch} from './utils';
|
|
|
import {getEncoding} from './encodings';
|
|
|
import {cache} from './decorators';
|
|
|
import {range} from './utils';
|
|
|
|
|
|
// iconv-lite is an optional dependency.
|
|
|
try {
|
|
|
var iconv = require('iconv-lite');
|
|
|
} catch (err) {}
|
|
|
|
|
|
export default class CmapProcessor {
|
|
|
constructor(cmapTable) {
|
|
|
// Attempt to find a Unicode cmap first
|
|
|
this.encoding = null;
|
|
|
this.cmap = this.findSubtable(cmapTable, [
|
|
|
// 32-bit subtables
|
|
|
[3, 10],
|
|
|
[0, 6],
|
|
|
[0, 4],
|
|
|
|
|
|
// 16-bit subtables
|
|
|
[3, 1],
|
|
|
[0, 3],
|
|
|
[0, 2],
|
|
|
[0, 1],
|
|
|
[0, 0]
|
|
|
]);
|
|
|
|
|
|
// If not unicode cmap was found, and iconv-lite is installed,
|
|
|
// take the first table with a supported encoding.
|
|
|
if (!this.cmap && iconv) {
|
|
|
for (let cmap of cmapTable.tables) {
|
|
|
let encoding = getEncoding(cmap.platformID, cmap.encodingID, cmap.table.language - 1);
|
|
|
if (iconv.encodingExists(encoding)) {
|
|
|
this.cmap = cmap.table;
|
|
|
this.encoding = encoding;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!this.cmap) {
|
|
|
throw new Error("Could not find a supported cmap table");
|
|
|
}
|
|
|
|
|
|
this.uvs = this.findSubtable(cmapTable, [[0, 5]]);
|
|
|
if (this.uvs && this.uvs.version !== 14) {
|
|
|
this.uvs = null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
findSubtable(cmapTable, pairs) {
|
|
|
for (let [platformID, encodingID] of pairs) {
|
|
|
for (let cmap of cmapTable.tables) {
|
|
|
if (cmap.platformID === platformID && cmap.encodingID === encodingID) {
|
|
|
return cmap.table;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
lookup(codepoint, variationSelector) {
|
|
|
// If there is no Unicode cmap in this font, we need to re-encode
|
|
|
// the codepoint in the encoding that the cmap supports.
|
|
|
if (this.encoding) {
|
|
|
let buf = iconv.encode(String.fromCodePoint(codepoint), this.encoding);
|
|
|
codepoint = 0;
|
|
|
for (let i = 0; i < buf.length; i++) {
|
|
|
codepoint = (codepoint << 8) | buf[i];
|
|
|
}
|
|
|
|
|
|
// Otherwise, try to get a Unicode variation selector for this codepoint if one is provided.
|
|
|
} else if (variationSelector) {
|
|
|
let gid = this.getVariationSelector(codepoint, variationSelector);
|
|
|
if (gid) {
|
|
|
return gid;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
let cmap = this.cmap;
|
|
|
switch (cmap.version) {
|
|
|
case 0:
|
|
|
return cmap.codeMap.get(codepoint) || 0;
|
|
|
|
|
|
case 4: {
|
|
|
let min = 0;
|
|
|
let max = cmap.segCount - 1;
|
|
|
while (min <= max) {
|
|
|
let mid = (min + max) >> 1;
|
|
|
|
|
|
if (codepoint < cmap.startCode.get(mid)) {
|
|
|
max = mid - 1;
|
|
|
} else if (codepoint > cmap.endCode.get(mid)) {
|
|
|
min = mid + 1;
|
|
|
} else {
|
|
|
let rangeOffset = cmap.idRangeOffset.get(mid);
|
|
|
let gid;
|
|
|
|
|
|
if (rangeOffset === 0) {
|
|
|
gid = codepoint + cmap.idDelta.get(mid);
|
|
|
} else {
|
|
|
let index = rangeOffset / 2 + (codepoint - cmap.startCode.get(mid)) - (cmap.segCount - mid);
|
|
|
gid = cmap.glyphIndexArray.get(index) || 0;
|
|
|
if (gid !== 0) {
|
|
|
gid += cmap.idDelta.get(mid);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return gid & 0xffff;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
case 8:
|
|
|
throw new Error('TODO: cmap format 8');
|
|
|
|
|
|
case 6:
|
|
|
case 10:
|
|
|
return cmap.glyphIndices.get(codepoint - cmap.firstCode) || 0;
|
|
|
|
|
|
case 12:
|
|
|
case 13: {
|
|
|
let min = 0;
|
|
|
let max = cmap.nGroups - 1;
|
|
|
while (min <= max) {
|
|
|
let mid = (min + max) >> 1;
|
|
|
let group = cmap.groups.get(mid);
|
|
|
|
|
|
if (codepoint < group.startCharCode) {
|
|
|
max = mid - 1;
|
|
|
} else if (codepoint > group.endCharCode) {
|
|
|
min = mid + 1;
|
|
|
} else {
|
|
|
if (cmap.version === 12) {
|
|
|
return group.glyphID + (codepoint - group.startCharCode);
|
|
|
} else {
|
|
|
return group.glyphID;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
case 14:
|
|
|
throw new Error('TODO: cmap format 14');
|
|
|
|
|
|
default:
|
|
|
throw new Error(`Unknown cmap format ${cmap.version}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
getVariationSelector(codepoint, variationSelector) {
|
|
|
if (!this.uvs) {
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
let selectors = this.uvs.varSelectors.toArray();
|
|
|
let i = binarySearch(selectors, x => variationSelector - x.varSelector);
|
|
|
let sel = selectors[i];
|
|
|
|
|
|
if (i !== -1 && sel.defaultUVS) {
|
|
|
i = binarySearch(sel.defaultUVS, x =>
|
|
|
codepoint < x.startUnicodeValue ? -1 : codepoint > x.startUnicodeValue + x.additionalCount ? +1 : 0
|
|
|
);
|
|
|
}
|
|
|
|
|
|
if (i !== -1 && sel.nonDefaultUVS) {
|
|
|
i = binarySearch(sel.nonDefaultUVS, x => codepoint - x.unicodeValue);
|
|
|
if (i !== -1) {
|
|
|
return sel.nonDefaultUVS[i].glyphID;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
@cache
|
|
|
getCharacterSet() {
|
|
|
let cmap = this.cmap;
|
|
|
switch (cmap.version) {
|
|
|
case 0:
|
|
|
return range(0, cmap.codeMap.length);
|
|
|
|
|
|
case 4: {
|
|
|
let res = [];
|
|
|
let endCodes = cmap.endCode.toArray();
|
|
|
for (let i = 0; i < endCodes.length; i++) {
|
|
|
let tail = endCodes[i] + 1;
|
|
|
let start = cmap.startCode.get(i);
|
|
|
res.push(...range(start, tail));
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
case 8:
|
|
|
throw new Error('TODO: cmap format 8');
|
|
|
|
|
|
case 6:
|
|
|
case 10:
|
|
|
return range(cmap.firstCode, cmap.firstCode + cmap.glyphIndices.length);
|
|
|
|
|
|
case 12:
|
|
|
case 13: {
|
|
|
let res = [];
|
|
|
for (let group of cmap.groups.toArray()) {
|
|
|
res.push(...range(group.startCharCode, group.endCharCode + 1));
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
case 14:
|
|
|
throw new Error('TODO: cmap format 14');
|
|
|
|
|
|
default:
|
|
|
throw new Error(`Unknown cmap format ${cmap.version}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@cache
|
|
|
codePointsForGlyph(gid) {
|
|
|
let cmap = this.cmap;
|
|
|
switch (cmap.version) {
|
|
|
case 0: {
|
|
|
let res = [];
|
|
|
for (let i = 0; i < 256; i++) {
|
|
|
if (cmap.codeMap.get(i) === gid) {
|
|
|
res.push(i);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
case 4: {
|
|
|
let res = [];
|
|
|
for (let i = 0; i < cmap.segCount; i++) {
|
|
|
let end = cmap.endCode.get(i);
|
|
|
let start = cmap.startCode.get(i);
|
|
|
let rangeOffset = cmap.idRangeOffset.get(i);
|
|
|
let delta = cmap.idDelta.get(i);
|
|
|
|
|
|
for (var c = start; c <= end; c++) {
|
|
|
let g = 0;
|
|
|
if (rangeOffset === 0) {
|
|
|
g = c + delta;
|
|
|
} else {
|
|
|
let index = rangeOffset / 2 + (c - start) - (cmap.segCount - i);
|
|
|
g = cmap.glyphIndexArray.get(index) || 0;
|
|
|
if (g !== 0) {
|
|
|
g += delta;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (g === gid) {
|
|
|
res.push(c);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
case 12: {
|
|
|
let res = [];
|
|
|
for (let group of cmap.groups.toArray()) {
|
|
|
if (gid >= group.glyphID && gid <= group.glyphID + (group.endCharCode - group.startCharCode)) {
|
|
|
res.push(group.startCharCode + (gid - group.glyphID));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
case 13: {
|
|
|
let res = [];
|
|
|
for (let group of cmap.groups.toArray()) {
|
|
|
if (gid === group.glyphID) {
|
|
|
res.push(...range(group.startCharCode, group.endCharCode + 1));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
default:
|
|
|
throw new Error(`Unknown cmap format ${cmap.version}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|