|
|
import unicode from 'unicode-properties';
|
|
|
|
|
|
/**
|
|
|
* This class is used when GPOS does not define 'mark' or 'mkmk' features
|
|
|
* for positioning marks relative to base glyphs. It uses the unicode
|
|
|
* combining class property to position marks.
|
|
|
*
|
|
|
* Based on code from Harfbuzz, thanks!
|
|
|
* https://github.com/behdad/harfbuzz/blob/master/src/hb-ot-shape-fallback.cc
|
|
|
*/
|
|
|
export default class UnicodeLayoutEngine {
|
|
|
constructor(font) {
|
|
|
this.font = font;
|
|
|
}
|
|
|
|
|
|
positionGlyphs(glyphs, positions) {
|
|
|
// find each base + mark cluster, and position the marks relative to the base
|
|
|
let clusterStart = 0;
|
|
|
let clusterEnd = 0;
|
|
|
for (let index = 0; index < glyphs.length; index++) {
|
|
|
let glyph = glyphs[index];
|
|
|
if (glyph.isMark) { // TODO: handle ligatures
|
|
|
clusterEnd = index;
|
|
|
} else {
|
|
|
if (clusterStart !== clusterEnd) {
|
|
|
this.positionCluster(glyphs, positions, clusterStart, clusterEnd);
|
|
|
}
|
|
|
|
|
|
clusterStart = clusterEnd = index;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (clusterStart !== clusterEnd) {
|
|
|
this.positionCluster(glyphs, positions, clusterStart, clusterEnd);
|
|
|
}
|
|
|
|
|
|
return positions;
|
|
|
}
|
|
|
|
|
|
positionCluster(glyphs, positions, clusterStart, clusterEnd) {
|
|
|
let base = glyphs[clusterStart];
|
|
|
let baseBox = base.cbox.copy();
|
|
|
|
|
|
// adjust bounding box for ligature glyphs
|
|
|
if (base.codePoints.length > 1) {
|
|
|
// LTR. TODO: RTL support.
|
|
|
baseBox.minX += ((base.codePoints.length - 1) * baseBox.width) / base.codePoints.length;
|
|
|
}
|
|
|
|
|
|
let xOffset = -positions[clusterStart].xAdvance;
|
|
|
let yOffset = 0;
|
|
|
let yGap = this.font.unitsPerEm / 16;
|
|
|
|
|
|
// position each of the mark glyphs relative to the base glyph
|
|
|
for (let index = clusterStart + 1; index <= clusterEnd; index++) {
|
|
|
let mark = glyphs[index];
|
|
|
let markBox = mark.cbox;
|
|
|
let position = positions[index];
|
|
|
|
|
|
let combiningClass = this.getCombiningClass(mark.codePoints[0]);
|
|
|
|
|
|
if (combiningClass !== 'Not_Reordered') {
|
|
|
position.xOffset = position.yOffset = 0;
|
|
|
|
|
|
// x positioning
|
|
|
switch (combiningClass) {
|
|
|
case 'Double_Above':
|
|
|
case 'Double_Below':
|
|
|
// LTR. TODO: RTL support.
|
|
|
position.xOffset += baseBox.minX - markBox.width / 2 - markBox.minX;
|
|
|
break;
|
|
|
|
|
|
case 'Attached_Below_Left':
|
|
|
case 'Below_Left':
|
|
|
case 'Above_Left':
|
|
|
// left align
|
|
|
position.xOffset += baseBox.minX - markBox.minX;
|
|
|
break;
|
|
|
|
|
|
case 'Attached_Above_Right':
|
|
|
case 'Below_Right':
|
|
|
case 'Above_Right':
|
|
|
// right align
|
|
|
position.xOffset += baseBox.maxX - markBox.width - markBox.minX;
|
|
|
break;
|
|
|
|
|
|
default: // Attached_Below, Attached_Above, Below, Above, other
|
|
|
// center align
|
|
|
position.xOffset += baseBox.minX + (baseBox.width - markBox.width) / 2 - markBox.minX;
|
|
|
}
|
|
|
|
|
|
// y positioning
|
|
|
switch (combiningClass) {
|
|
|
case 'Double_Below':
|
|
|
case 'Below_Left':
|
|
|
case 'Below':
|
|
|
case 'Below_Right':
|
|
|
case 'Attached_Below_Left':
|
|
|
case 'Attached_Below':
|
|
|
// add a small gap between the glyphs if they are not attached
|
|
|
if (combiningClass === 'Attached_Below_Left' || combiningClass === 'Attached_Below') {
|
|
|
baseBox.minY += yGap;
|
|
|
}
|
|
|
|
|
|
position.yOffset = -baseBox.minY - markBox.maxY;
|
|
|
baseBox.minY += markBox.height;
|
|
|
break;
|
|
|
|
|
|
case 'Double_Above':
|
|
|
case 'Above_Left':
|
|
|
case 'Above':
|
|
|
case 'Above_Right':
|
|
|
case 'Attached_Above':
|
|
|
case 'Attached_Above_Right':
|
|
|
// add a small gap between the glyphs if they are not attached
|
|
|
if (combiningClass === 'Attached_Above' || combiningClass === 'Attached_Above_Right') {
|
|
|
baseBox.maxY += yGap;
|
|
|
}
|
|
|
|
|
|
position.yOffset = baseBox.maxY - markBox.minY;
|
|
|
baseBox.maxY += markBox.height;
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
position.xAdvance = position.yAdvance = 0;
|
|
|
position.xOffset += xOffset;
|
|
|
position.yOffset += yOffset;
|
|
|
|
|
|
} else {
|
|
|
xOffset -= position.xAdvance;
|
|
|
yOffset -= position.yAdvance;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
getCombiningClass(codePoint) {
|
|
|
let combiningClass = unicode.getCombiningClass(codePoint);
|
|
|
|
|
|
// Thai / Lao need some per-character work
|
|
|
if ((codePoint & ~0xff) === 0x0e00) {
|
|
|
if (combiningClass === 'Not_Reordered') {
|
|
|
switch (codePoint) {
|
|
|
case 0x0e31:
|
|
|
case 0x0e34:
|
|
|
case 0x0e35:
|
|
|
case 0x0e36:
|
|
|
case 0x0e37:
|
|
|
case 0x0e47:
|
|
|
case 0x0e4c:
|
|
|
case 0x0e3d:
|
|
|
case 0x0e4e:
|
|
|
return 'Above_Right';
|
|
|
|
|
|
case 0x0eb1:
|
|
|
case 0x0eb4:
|
|
|
case 0x0eb5:
|
|
|
case 0x0eb6:
|
|
|
case 0x0eb7:
|
|
|
case 0x0ebb:
|
|
|
case 0x0ecc:
|
|
|
case 0x0ecd:
|
|
|
return 'Above';
|
|
|
|
|
|
case 0x0ebc:
|
|
|
return 'Below';
|
|
|
}
|
|
|
} else if (codePoint === 0x0e3a) { // virama
|
|
|
return 'Below_Right';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
switch (combiningClass) {
|
|
|
// Hebrew
|
|
|
|
|
|
case 'CCC10': // sheva
|
|
|
case 'CCC11': // hataf segol
|
|
|
case 'CCC12': // hataf patah
|
|
|
case 'CCC13': // hataf qamats
|
|
|
case 'CCC14': // hiriq
|
|
|
case 'CCC15': // tsere
|
|
|
case 'CCC16': // segol
|
|
|
case 'CCC17': // patah
|
|
|
case 'CCC18': // qamats
|
|
|
case 'CCC20': // qubuts
|
|
|
case 'CCC22': // meteg
|
|
|
return 'Below';
|
|
|
|
|
|
case 'CCC23': // rafe
|
|
|
return 'Attached_Above';
|
|
|
|
|
|
case 'CCC24': // shin dot
|
|
|
return 'Above_Right';
|
|
|
|
|
|
case 'CCC25': // sin dot
|
|
|
case 'CCC19': // holam
|
|
|
return 'Above_Left';
|
|
|
|
|
|
case 'CCC26': // point varika
|
|
|
return 'Above';
|
|
|
|
|
|
case 'CCC21': // dagesh
|
|
|
break;
|
|
|
|
|
|
// Arabic and Syriac
|
|
|
|
|
|
case 'CCC27': // fathatan
|
|
|
case 'CCC28': // dammatan
|
|
|
case 'CCC30': // fatha
|
|
|
case 'CCC31': // damma
|
|
|
case 'CCC33': // shadda
|
|
|
case 'CCC34': // sukun
|
|
|
case 'CCC35': // superscript alef
|
|
|
case 'CCC36': // superscript alaph
|
|
|
return 'Above';
|
|
|
|
|
|
case 'CCC29': // kasratan
|
|
|
case 'CCC32': // kasra
|
|
|
return 'Below';
|
|
|
|
|
|
// Thai
|
|
|
|
|
|
case 'CCC103': // sara u / sara uu
|
|
|
return 'Below_Right';
|
|
|
|
|
|
case 'CCC107': // mai
|
|
|
return 'Above_Right';
|
|
|
|
|
|
// Lao
|
|
|
|
|
|
case 'CCC118': // sign u / sign uu
|
|
|
return 'Below';
|
|
|
|
|
|
case 'CCC122': // mai
|
|
|
return 'Above';
|
|
|
|
|
|
// Tibetan
|
|
|
|
|
|
case 'CCC129': // sign aa
|
|
|
case 'CCC132': // sign u
|
|
|
return 'Below';
|
|
|
|
|
|
case 'CCC130': // sign i
|
|
|
return 'Above';
|
|
|
}
|
|
|
|
|
|
return combiningClass;
|
|
|
}
|
|
|
}
|
|
|
|