first commit

This commit is contained in:
2026-07-19 03:44:35 +09:00
commit 7f950339ae
23281 changed files with 3217138 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
/**
* Represents positioning information for a glyph in a GlyphRun.
*/
export default class GlyphPosition {
constructor(xAdvance = 0, yAdvance = 0, xOffset = 0, yOffset = 0) {
/**
* The amount to move the virtual pen in the X direction after rendering this glyph.
* @type {number}
*/
this.xAdvance = xAdvance;
/**
* The amount to move the virtual pen in the Y direction after rendering this glyph.
* @type {number}
*/
this.yAdvance = yAdvance;
/**
* The offset from the pen position in the X direction at which to render this glyph.
* @type {number}
*/
this.xOffset = xOffset;
/**
* The offset from the pen position in the Y direction at which to render this glyph.
* @type {number}
*/
this.yOffset = yOffset;
}
}
+108
View File
@@ -0,0 +1,108 @@
import BBox from '../glyph/BBox';
import * as Script from '../layout/Script';
/**
* Represents a run of Glyph and GlyphPosition objects.
* Returned by the font layout method.
*/
export default class GlyphRun {
constructor(glyphs, features, script, language, direction) {
/**
* An array of Glyph objects in the run
* @type {Glyph[]}
*/
this.glyphs = glyphs;
/**
* An array of GlyphPosition objects for each glyph in the run
* @type {GlyphPosition[]}
*/
this.positions = null;
/**
* The script that was requested for shaping. This was either passed in or detected automatically.
* @type {string}
*/
this.script = script;
/**
* The language requested for shaping, as passed in. If `null`, the default language for the
* script was used.
* @type {string}
*/
this.language = language || null;
/**
* The direction requested for shaping, as passed in (either ltr or rtl).
* If `null`, the default direction of the script is used.
* @type {string}
*/
this.direction = direction || Script.direction(script);
/**
* The features requested during shaping. This is a combination of user
* specified features and features chosen by the shaper.
* @type {object}
*/
this.features = {};
// Convert features to an object
if (Array.isArray(features)) {
for (let tag of features) {
this.features[tag] = true;
}
} else if (typeof features === 'object') {
this.features = features;
}
}
/**
* The total advance width of the run.
* @type {number}
*/
get advanceWidth() {
let width = 0;
for (let position of this.positions) {
width += position.xAdvance;
}
return width;
}
/**
* The total advance height of the run.
* @type {number}
*/
get advanceHeight() {
let height = 0;
for (let position of this.positions) {
height += position.yAdvance;
}
return height;
}
/**
* The bounding box containing all glyphs in the run.
* @type {BBox}
*/
get bbox() {
let bbox = new BBox;
let x = 0;
let y = 0;
for (let index = 0; index < this.glyphs.length; index++) {
let glyph = this.glyphs[index];
let p = this.positions[index];
let b = glyph.bbox;
bbox.addPoint(b.minX + x + p.xOffset, b.minY + y + p.yOffset);
bbox.addPoint(b.maxX + x + p.xOffset, b.maxY + y + p.yOffset);
x += p.xAdvance;
y += p.yAdvance;
}
return bbox;
}
}
+94
View File
@@ -0,0 +1,94 @@
import {binarySearch} from '../utils';
export default class KernProcessor {
constructor(font) {
this.kern = font.kern;
}
process(glyphs, positions) {
for (let glyphIndex = 0; glyphIndex < glyphs.length - 1; glyphIndex++) {
let left = glyphs[glyphIndex].id;
let right = glyphs[glyphIndex + 1].id;
positions[glyphIndex].xAdvance += this.getKerning(left, right);
}
}
getKerning(left, right) {
let res = 0;
for (let table of this.kern.tables) {
if (table.coverage.crossStream) {
continue;
}
switch (table.version) {
case 0:
if (!table.coverage.horizontal) {
continue;
}
break;
case 1:
if (table.coverage.vertical || table.coverage.variation) {
continue;
}
break;
default:
throw new Error(`Unsupported kerning table version ${table.version}`);
}
let val = 0;
let s = table.subtable;
switch (table.format) {
case 0:
let pairIdx = binarySearch(s.pairs, function (pair) {
return (left - pair.left) || (right - pair.right);
});
if (pairIdx >= 0) {
val = s.pairs[pairIdx].value;
}
break;
case 2:
let leftOffset = 0, rightOffset = 0;
if (left >= s.leftTable.firstGlyph && left < s.leftTable.firstGlyph + s.leftTable.nGlyphs) {
leftOffset = s.leftTable.offsets[left - s.leftTable.firstGlyph];
} else {
leftOffset = s.array.off;
}
if (right >= s.rightTable.firstGlyph && right < s.rightTable.firstGlyph + s.rightTable.nGlyphs) {
rightOffset = s.rightTable.offsets[right - s.rightTable.firstGlyph];
}
let index = (leftOffset + rightOffset - s.array.off) / 2;
val = s.array.values.get(index);
break;
case 3:
if (left >= s.glyphCount || right >= s.glyphCount) {
return 0;
}
val = s.kernValue[s.kernIndex[s.leftClass[left] * s.rightClassCount + s.rightClass[right]]];
break;
default:
throw new Error(`Unsupported kerning sub-table format ${table.format}`);
}
// Microsoft supports the override flag, which resets the result
// Otherwise, the sum of the results from all subtables is returned
if (table.coverage.override) {
res = val;
} else {
res += val;
}
}
return res;
}
}
+189
View File
@@ -0,0 +1,189 @@
import KernProcessor from './KernProcessor';
import UnicodeLayoutEngine from './UnicodeLayoutEngine';
import GlyphRun from './GlyphRun';
import GlyphPosition from './GlyphPosition';
import * as Script from './Script';
import AATLayoutEngine from '../aat/AATLayoutEngine';
import OTLayoutEngine from '../opentype/OTLayoutEngine';
export default class LayoutEngine {
constructor(font) {
this.font = font;
this.unicodeLayoutEngine = null;
this.kernProcessor = null;
// Choose an advanced layout engine. We try the AAT morx table first since more
// scripts are currently supported because the shaping logic is built into the font.
if (this.font.morx) {
this.engine = new AATLayoutEngine(this.font);
} else if (this.font.GSUB || this.font.GPOS) {
this.engine = new OTLayoutEngine(this.font);
}
}
layout(string, features, script, language, direction) {
// Make the features parameter optional
if (typeof features === 'string') {
direction = language;
language = script;
script = features;
features = [];
}
// Map string to glyphs if needed
if (typeof string === 'string') {
// Attempt to detect the script from the string if not provided.
if (script == null) {
script = Script.forString(string);
}
var glyphs = this.font.glyphsForString(string);
} else {
// Attempt to detect the script from the glyph code points if not provided.
if (script == null) {
let codePoints = [];
for (let glyph of string) {
codePoints.push(...glyph.codePoints);
}
script = Script.forCodePoints(codePoints);
}
var glyphs = string;
}
let glyphRun = new GlyphRun(glyphs, features, script, language, direction);
// Return early if there are no glyphs
if (glyphs.length === 0) {
glyphRun.positions = [];
return glyphRun;
}
// Setup the advanced layout engine
if (this.engine && this.engine.setup) {
this.engine.setup(glyphRun);
}
// Substitute and position the glyphs
this.substitute(glyphRun);
this.position(glyphRun);
this.hideDefaultIgnorables(glyphRun.glyphs, glyphRun.positions);
// Let the layout engine clean up any state it might have
if (this.engine && this.engine.cleanup) {
this.engine.cleanup();
}
return glyphRun;
}
substitute(glyphRun) {
// Call the advanced layout engine to make substitutions
if (this.engine && this.engine.substitute) {
this.engine.substitute(glyphRun);
}
}
position(glyphRun) {
// Get initial glyph positions
glyphRun.positions = glyphRun.glyphs.map(glyph => new GlyphPosition(glyph.advanceWidth));
let positioned = null;
// Call the advanced layout engine. Returns the features applied.
if (this.engine && this.engine.position) {
positioned = this.engine.position(glyphRun);
}
// if there is no GPOS table, use unicode properties to position marks.
if (!positioned && (!this.engine || this.engine.fallbackPosition)) {
if (!this.unicodeLayoutEngine) {
this.unicodeLayoutEngine = new UnicodeLayoutEngine(this.font);
}
this.unicodeLayoutEngine.positionGlyphs(glyphRun.glyphs, glyphRun.positions);
}
// if kerning is not supported by GPOS, do kerning with the TrueType/AAT kern table
if ((!positioned || !positioned.kern) && glyphRun.features.kern !== false && this.font.kern) {
if (!this.kernProcessor) {
this.kernProcessor = new KernProcessor(this.font);
}
this.kernProcessor.process(glyphRun.glyphs, glyphRun.positions);
glyphRun.features.kern = true;
}
}
hideDefaultIgnorables(glyphs, positions) {
let space = this.font.glyphForCodePoint(0x20);
for (let i = 0; i < glyphs.length; i++) {
if (this.isDefaultIgnorable(glyphs[i].codePoints[0])) {
glyphs[i] = space;
positions[i].xAdvance = 0;
positions[i].yAdvance = 0;
}
}
}
isDefaultIgnorable(ch) {
// From DerivedCoreProperties.txt in the Unicode database,
// minus U+115F, U+1160, U+3164 and U+FFA0, which is what
// Harfbuzz and Uniscribe do.
let plane = ch >> 16;
if (plane === 0) {
// BMP
switch (ch >> 8) {
case 0x00: return ch === 0x00AD;
case 0x03: return ch === 0x034F;
case 0x06: return ch === 0x061C;
case 0x17: return 0x17B4 <= ch && ch <= 0x17B5;
case 0x18: return 0x180B <= ch && ch <= 0x180E;
case 0x20: return (0x200B <= ch && ch <= 0x200F) || (0x202A <= ch && ch <= 0x202E) || (0x2060 <= ch && ch <= 0x206F);
case 0xFE: return (0xFE00 <= ch && ch <= 0xFE0F) || ch === 0xFEFF;
case 0xFF: return 0xFFF0 <= ch && ch <= 0xFFF8;
default: return false;
}
} else {
// Other planes
switch (plane) {
case 0x01: return (0x1BCA0 <= ch && ch <= 0x1BCA3) || (0x1D173 <= ch && ch <= 0x1D17A);
case 0x0E: return 0xE0000 <= ch && ch <= 0xE0FFF;
default: return false;
}
}
}
getAvailableFeatures(script, language) {
let features = [];
if (this.engine) {
features.push(...this.engine.getAvailableFeatures(script, language));
}
if (this.font.kern && features.indexOf('kern') === -1) {
features.push('kern');
}
return features;
}
stringsForGlyph(gid) {
let result = new Set;
let codePoints = this.font._cmapProcessor.codePointsForGlyph(gid);
for (let codePoint of codePoints) {
result.add(String.fromCodePoint(codePoint));
}
if (this.engine && this.engine.stringsForGlyph) {
for (let string of this.engine.stringsForGlyph(gid)) {
result.add(string);
}
}
return Array.from(result);
}
}
+231
View File
@@ -0,0 +1,231 @@
import {getScript} from 'unicode-properties';
// This maps the Unicode Script property to an OpenType script tag
// Data from http://www.microsoft.com/typography/otspec/scripttags.htm
// and http://www.unicode.org/Public/UNIDATA/PropertyValueAliases.txt.
const UNICODE_SCRIPTS = {
Caucasian_Albanian: 'aghb',
Arabic: 'arab',
Imperial_Aramaic: 'armi',
Armenian: 'armn',
Avestan: 'avst',
Balinese: 'bali',
Bamum: 'bamu',
Bassa_Vah: 'bass',
Batak: 'batk',
Bengali: ['bng2', 'beng'],
Bopomofo: 'bopo',
Brahmi: 'brah',
Braille: 'brai',
Buginese: 'bugi',
Buhid: 'buhd',
Chakma: 'cakm',
Canadian_Aboriginal: 'cans',
Carian: 'cari',
Cham: 'cham',
Cherokee: 'cher',
Coptic: 'copt',
Cypriot: 'cprt',
Cyrillic: 'cyrl',
Devanagari: ['dev2', 'deva'],
Deseret: 'dsrt',
Duployan: 'dupl',
Egyptian_Hieroglyphs: 'egyp',
Elbasan: 'elba',
Ethiopic: 'ethi',
Georgian: 'geor',
Glagolitic: 'glag',
Gothic: 'goth',
Grantha: 'gran',
Greek: 'grek',
Gujarati: ['gjr2', 'gujr'],
Gurmukhi: ['gur2', 'guru'],
Hangul: 'hang',
Han: 'hani',
Hanunoo: 'hano',
Hebrew: 'hebr',
Hiragana: 'hira',
Pahawh_Hmong: 'hmng',
Katakana_Or_Hiragana: 'hrkt',
Old_Italic: 'ital',
Javanese: 'java',
Kayah_Li: 'kali',
Katakana: 'kana',
Kharoshthi: 'khar',
Khmer: 'khmr',
Khojki: 'khoj',
Kannada: ['knd2', 'knda'],
Kaithi: 'kthi',
Tai_Tham: 'lana',
Lao: 'lao ',
Latin: 'latn',
Lepcha: 'lepc',
Limbu: 'limb',
Linear_A: 'lina',
Linear_B: 'linb',
Lisu: 'lisu',
Lycian: 'lyci',
Lydian: 'lydi',
Mahajani: 'mahj',
Mandaic: 'mand',
Manichaean: 'mani',
Mende_Kikakui: 'mend',
Meroitic_Cursive: 'merc',
Meroitic_Hieroglyphs: 'mero',
Malayalam: ['mlm2', 'mlym'],
Modi: 'modi',
Mongolian: 'mong',
Mro: 'mroo',
Meetei_Mayek: 'mtei',
Myanmar: ['mym2', 'mymr'],
Old_North_Arabian: 'narb',
Nabataean: 'nbat',
Nko: 'nko ',
Ogham: 'ogam',
Ol_Chiki: 'olck',
Old_Turkic: 'orkh',
Oriya: ['ory2', 'orya'],
Osmanya: 'osma',
Palmyrene: 'palm',
Pau_Cin_Hau: 'pauc',
Old_Permic: 'perm',
Phags_Pa: 'phag',
Inscriptional_Pahlavi: 'phli',
Psalter_Pahlavi: 'phlp',
Phoenician: 'phnx',
Miao: 'plrd',
Inscriptional_Parthian: 'prti',
Rejang: 'rjng',
Runic: 'runr',
Samaritan: 'samr',
Old_South_Arabian: 'sarb',
Saurashtra: 'saur',
Shavian: 'shaw',
Sharada: 'shrd',
Siddham: 'sidd',
Khudawadi: 'sind',
Sinhala: 'sinh',
Sora_Sompeng: 'sora',
Sundanese: 'sund',
Syloti_Nagri: 'sylo',
Syriac: 'syrc',
Tagbanwa: 'tagb',
Takri: 'takr',
Tai_Le: 'tale',
New_Tai_Lue: 'talu',
Tamil: ['tml2', 'taml'],
Tai_Viet: 'tavt',
Telugu: ['tel2', 'telu'],
Tifinagh: 'tfng',
Tagalog: 'tglg',
Thaana: 'thaa',
Thai: 'thai',
Tibetan: 'tibt',
Tirhuta: 'tirh',
Ugaritic: 'ugar',
Vai: 'vai ',
Warang_Citi: 'wara',
Old_Persian: 'xpeo',
Cuneiform: 'xsux',
Yi: 'yi ',
Inherited: 'zinh',
Common: 'zyyy',
Unknown: 'zzzz'
};
const OPENTYPE_SCRIPTS = {};
for (let script in UNICODE_SCRIPTS) {
let tag = UNICODE_SCRIPTS[script];
if (Array.isArray(tag)) {
for (let t of tag) {
OPENTYPE_SCRIPTS[t] = script;
}
} else {
OPENTYPE_SCRIPTS[tag] = script;
}
}
export function fromUnicode(script) {
return UNICODE_SCRIPTS[script];
}
export function fromOpenType(tag) {
return OPENTYPE_SCRIPTS[tag];
}
export function forString(string) {
let len = string.length;
let idx = 0;
while (idx < len) {
let code = string.charCodeAt(idx++);
// Check if this is a high surrogate
if (0xd800 <= code && code <= 0xdbff && idx < len) {
let next = string.charCodeAt(idx);
// Check if this is a low surrogate
if (0xdc00 <= next && next <= 0xdfff) {
idx++;
code = ((code & 0x3FF) << 10) + (next & 0x3FF) + 0x10000;
}
}
let script = getScript(code);
if (script !== 'Common' && script !== 'Inherited' && script !== 'Unknown') {
return UNICODE_SCRIPTS[script];
}
}
return UNICODE_SCRIPTS.Unknown;
}
export function forCodePoints(codePoints) {
for (let i = 0; i < codePoints.length; i++) {
let codePoint = codePoints[i];
let script = getScript(codePoint);
if (script !== 'Common' && script !== 'Inherited' && script !== 'Unknown') {
return UNICODE_SCRIPTS[script];
}
}
return UNICODE_SCRIPTS.Unknown;
}
// The scripts in this map are written from right to left
const RTL = {
arab: true, // Arabic
hebr: true, // Hebrew
syrc: true, // Syriac
thaa: true, // Thaana
cprt: true, // Cypriot Syllabary
khar: true, // Kharosthi
phnx: true, // Phoenician
'nko ': true, // N'Ko
lydi: true, // Lydian
avst: true, // Avestan
armi: true, // Imperial Aramaic
phli: true, // Inscriptional Pahlavi
prti: true, // Inscriptional Parthian
sarb: true, // Old South Arabian
orkh: true, // Old Turkic, Orkhon Runic
samr: true, // Samaritan
mand: true, // Mandaic, Mandaean
merc: true, // Meroitic Cursive
mero: true, // Meroitic Hieroglyphs
// Unicode 7.0 (not listed on http://www.microsoft.com/typography/otspec/scripttags.htm)
mani: true, // Manichaean
mend: true, // Mende Kikakui
nbat: true, // Nabataean
narb: true, // Old North Arabian
palm: true, // Palmyrene
phlp: true // Psalter Pahlavi
};
export function direction(script) {
if (RTL[script]) {
return 'rtl';
}
return 'ltr';
}
+250
View File
@@ -0,0 +1,250 @@
import {getCombiningClass} 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 = 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;
}
}