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 | 1
1
6
6
6
90
9
81
7
81
6
1
6
8
8
8
3
8
8
8
8
9
9
9
9
9
9
9
8
9
9
9
9
1
| unicode = require '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
class UnicodeLayoutEngine
constructor: (@font) ->
positionGlyphs: (glyphs, positions) ->
# find each base + mark cluster, and position the marks relative to the base
clusterStart = 0
clusterEnd = 0
for glyph, index in glyphs
if glyph.isMark # TODO: handle ligatures
clusterEnd = index
else
if clusterStart isnt clusterEnd
@positionCluster glyphs, positions, clusterStart, clusterEnd
clusterStart = clusterEnd = index
if clusterStart isnt clusterEnd
@positionCluster glyphs, positions, clusterStart, clusterEnd
return positions
positionCluster: (glyphs, positions, clusterStart, clusterEnd) ->
base = glyphs[clusterStart]
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
xOffset = -positions[clusterStart].xAdvance
yOffset = 0
yGap = @font.unitsPerEm / 16
# position each of the mark glyphs relative to the base glyph
for index in [clusterStart + 1..clusterEnd] by 1
mark = glyphs[index]
markBox = mark.cbox
position = positions[index]
combiningClass = @getCombiningClass mark.codePoints[0]
Iif combiningClass isnt 'Not_Reordered'
position.xOffset = position.yOffset = 0
# x positioning
switch combiningClass
when 'Double_Above', 'Double_Below'
# LTR. TODO: RTL support.
position.xOffset += baseBox.minX - markBox.width / 2 - markBox.minX
when 'Attached_Below_Left', 'Below_Left', 'Above_Left'
# left align
position.xOffset += baseBox.minX - markBox.minX
when 'Attached_Above_Right', 'Below_Right', 'Above_Right'
# right align
position.xOffset += baseBox.maxX - markBox.width - markBox.minX
else # Attached_Below, Attached_Above, Below, Above, other
# center align
position.xOffset += baseBox.minX + (baseBox.width - markBox.width) / 2 - markBox.minX
# y positioning
switch combiningClass
when 'Double_Below', 'Below_Left', 'Below', 'Below_Right', 'Attached_Below_Left', 'Attached_Below'
# add a small gap between the glyphs if they are not attached
if combiningClass not in ['Attached_Below_Left', 'Attached_Below']
baseBox.minY += yGap
position.yOffset = -baseBox.minY - markBox.maxY
baseBox.minY += markBox.height
when 'Double_Above', 'Above_Left', 'Above', 'Above_Right', 'Attached_Above', 'Attached_Above_Right'
# add a small gap between the glyphs if they are not attached
if combiningClass not in ['Attached_Above', 'Attached_Above_Right']
baseBox.maxY += yGap
position.yOffset = baseBox.maxY - markBox.minY
baseBox.maxY += markBox.height
position.xAdvance = position.yAdvance = 0
position.xOffset += xOffset
position.yOffset += yOffset
else
xOffset -= position.xAdvance
yOffset -= position.yAdvance
return
getCombiningClass: (codePoint) ->
combiningClass = unicode.getCombiningClass codePoint
# Thai / Lao need some per-character work
Iif (codePoint & ~0xff) is 0x0e00
if combiningClass is 'Not_Reordered'
switch codePoint
when 0x0e31, 0x0e34, 0x0e35, 0x0e36, 0x0e37, 0x0e47, 0x0e4c, 0x0e3d, 0x0e4e
return 'Above_Right'
when 0x0eb1, 0x0eb4, 0x0eb5, 0x0eb6, 0x0eb7, 0x0ebb, 0x0ecc, 0x0ecd
return 'Above'
when 0x0ebc
return 'Below'
else if codePoint is 0x0e3a # virama
return 'Below_Right'
switch combiningClass
# Hebrew
# sheva, hataf segol, hataf patah, hataf qamats, hiriq, tsere, segol, patah, qamats, qubuts, meteg
when 'CCC10', 'CCC11', 'CCC12', 'CCC13', 'CCC14', 'CCC15', 'CCC16', 'CCC17', 'CCC18', 'CCC20', 'CCC22'
return 'Below'
when 'CCC23' # rafe
return 'Attached_Above'
when 'CCC24' # shin dot
return 'Above_Right'
when 'CCC25', 'CCC19' # sin dot, holam
return 'Above_Left'
when 'CCC26' # point varika
return 'Above'
when 'CCC21' # dagesh
break
# Arabic and Syriac
# fathatan, dammatan, fatha, damma, shadda, sukun, superscript alef, superscript alaph
when 'CCC27', 'CCC28', 'CCC30', 'CCC31', 'CCC33', 'CCC34', 'CCC35', 'CCC36'
return 'Above'
when 'CCC29', 'CCC32' # kasratan, kasra
return 'Below'
# Thai
when 'CCC103' # sara u / sara uu
return 'Below_Right'
when 'CCC107' # mai
return 'Above_Right'
# Lao
when 'CCC118' # sign u / sign uu
return 'Below'
when 'CCC122' # mai
return 'Above'
# Tibetan
when 'CCC129', 'CCC132' # sign aa, sign u
return 'Below'
when 'CCC130' # sign i
return 'Above'
return combiningClass
module.exports = UnicodeLayoutEngine
|