1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186 | 1
1
1
1
1
1
1
1
1
1
1
1
23
23
23
23
23
23
19
19
19
205
19
23
23
358
23
23
23
23
23
4
4
4
4
4
19
19
19
19
23
1
23
20
224
3
23
23
239
23
18
18
18
23
20
224
20
4
4
23
23
5
5
23
3
3
23
2
2
1
1
1
2
1
1
1
2
1
1
1
2
1
2
1
| GSUBProcessor = require '../opentype/GSUBProcessor'
GPOSProcessor = require '../opentype/GPOSProcessor'
GlyphInfo = require '../opentype/GlyphInfo'
Shapers = require '../opentype/shapers'
AATFeatureMap = require '../aat/AATFeatureMap'
AATMorxProcessor = require '../aat/AATMorxProcessor'
KernProcessor = require './KernProcessor'
UnicodeLayoutEngine = require './UnicodeLayoutEngine'
GlyphRun = require './GlyphRun'
Script = require './Script'
unicode = require 'unicode-properties'
class LayoutEngine
constructor: (@font) ->
layout: (string, features = [], script, language) ->
# Make the userFeatures parameter optional
Iif typeof features is 'string'
script = features
language = script
features = []
# Map string to glyphs if needed
Eif typeof string is 'string'
# Attempt to detect the script from the string if not provided.
script ?= Script.forString string
glyphs = @font.glyphsForString string
else
# Attempt to detect the script from the glyph code points if not provided.
unless script?
codePoints = []
for glyph in string
codePoints.push glyph.codePoints...
script = Script.forCodePoints codePoints
glyphs = string
# Return early if there are no glyphs
Iif glyphs.length is 0
return new GlyphRun glyphs, []
if not @font.morx and (@font.GSUB or @font.GPOS)
shaper = Shapers.choose script
features.push shaper.getGlobalFeatures(script)...
# Map glyphs to GlyphInfo objects so data can be passed between
# GSUB and GPOS without mutating the real (shared) Glyph objects.
glyphs = for glyph, i in glyphs
new GlyphInfo glyph.id, [glyph.codePoints...], features
features.push shaper.assignFeatures(glyphs, script, @font)...
# Remove duplicate features
featureMap = {}
for feature in features
featureMap[feature] = true
features = Object.keys(featureMap)
# Substitute and position the glyphs
glyphs = @substitute glyphs, features, script
positions = @position glyphs, features, script
# Remove default ignorables
# for i in [glyphs.length - 1..0] by -1
# glyph = glyphs[i]
# if glyph.codePoints.length is 1 and isDefaultIgnorable glyph.codePoints[0]
# glyphs.splice i, 1, @font.glyphForCodePoint(' '.charCodeAt(0))
# positions[i].xAdvance = 0
return new GlyphRun glyphs, positions
# isDefaultIgnorable = (codePoint) ->
# # From DerivedCoreProperties.txt in the Unicode database,
# # minus U+115F, U+1160, U+3164 and U+FFA0, which is what
# # Harfbuzz and Uniscribe do.
# codePoint in [0x00ad, 0x034f, 0x061c, 0xfeff] or
# 0x17b4 <= codePoint <= 0x17b5 or
# 0x180b <= codePoint <= 0x180e or
# 0x200b <= codePoint <= 0x200f or
# 0x202a <= codePoint <= 0x202e or
# 0x2060 <= codePoint <= 0x206f or
# 0xfe00 <= codePoint <= 0xfe0f or
# 0xfff0 <= codePoint <= 0xfff8 or
# 0x1bca0 <= codePoint <= 0x1bca3 or
# 0x1d173 <= codePoint <= 0x1d17a or
# 0xe0000 <= codePoint <= 0xe0fff
substitute: (glyphs, features, script, language) ->
# First, try AAT morx table.
# We do this first since more scripts are currently supported by AAT
# because the shaping logic is built into the font.
if @font.morx
# AAT expects the glyphs to be reversed prior to morx processing,
# so reverse the glyphs if the script is right-to-left.
isRTL = Script.direction(script) is 'rtl'
Iif isRTL
glyphs.reverse()
@morxProcessor ?= new AATMorxProcessor(@font)
@morxProcessor.process(glyphs, AATFeatureMap.mapOTToAAT(features))
# It is very unlikely, but possible for a font to have an AAT morx table
# along with an OpenType GPOS table. If so, reverse the glyphs again for
# GPOS, which expects glyphs to be in logical order.
Iif isRTL and @font.GPOS
glyphs.reverse()
# If not found, try the OpenType GSUB table.
else Eif @font.GSUB
@GSUBProcessor ?= new GSUBProcessor(@font, @font.GSUB)
@GSUBProcessor.selectScript script, language
@GSUBProcessor.applyFeatures(features, glyphs)
return glyphs
class GlyphPosition
constructor: (@xAdvance = 0, @yAdvance = 0, @xOffset = 0, @yOffset = 0) ->
position: (glyphs, features, script, language) ->
realGlyphs = if @font.GPOS or @font.GSUB
# Map the GlyphInfo objects back to real Glyph objects
for glyph, i in glyphs
@font.getGlyph glyph.id, glyph.codePoints
else
glyphs
positions = []
for glyph, i in glyphs
positions.push new GlyphPosition realGlyphs[i].advanceWidth
if @font.GPOS
@GPOSProcessor ?= new GPOSProcessor(@font, @font.GPOS)
@GPOSProcessor.selectScript script, language
@GPOSProcessor.applyFeatures(features, glyphs, positions)
if @font.GPOS or @font.GSUB
# Restore the real Glyph objects
for realGlyph, i in realGlyphs
glyphs[i] = realGlyph
# Reverse the glyphs and positions if the script is right-to-left
if Script.direction(script) is 'rtl'
glyphs.reverse()
positions.reverse()
gposFeatures = @GPOSProcessor?.features or {}
# if there is no GPOS table, use unicode properties to position marks.
unless @font.GPOS
@unicodeLayoutEngine ?= new UnicodeLayoutEngine @font
@unicodeLayoutEngine.positionGlyphs glyphs, positions
# if kerning is not supported by GPOS, do kerning with the TrueType/AAT kern table
if 'kern' not of gposFeatures and @font.kern
@kernProcessor ?= new KernProcessor @font
@kernProcessor.process glyphs, positions
return positions
getAvailableFeatures: (script, language) ->
features = []
if @font.GSUB
@GSUBProcessor ?= new GSUBProcessor @font, @font.GSUB
@GSUBProcessor.selectScript script, language
features.push Object.keys(@GSUBProcessor.features)...
if @font.GPOS
@GPOSProcessor ?= new GPOSProcessor @font, @font.GPOS
@GPOSProcessor.selectScript script, language
features.push Object.keys(@GPOSProcessor.features)...
if @font.morx
@morxProcessor ?= new AATMorxProcessor @font
aatFeatures = AATFeatureMap.mapAATToOT @morxProcessor.getSupportedFeatures()
features.push aatFeatures...
if @font.kern and (not @font.GPOS or 'kern' not of @GPOSProcessor.features)
features.push 'kern'
return features
module.exports = LayoutEngine
|