Skip to content

Commit 6656977

Browse files
authored
Merge pull request #819 from googlefonts/math-table
outlineCompiler: Support generating MATH table
2 parents 5586805 + 10937d4 commit 6656977

27 files changed

+747
-2
lines changed

Lib/ufo2ft/constants.py

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ class CFFOptimization(IntEnum):
4343
# ]
4444
COLR_CLIP_BOXES_KEY = UFO2FT_PREFIX + "colrClipBoxes"
4545

46+
GLYPHS_MATH_PREFIX = "com.nagwa.MATHPlugin."
47+
GLYPHS_MATH_CONSTANTS_KEY = GLYPHS_MATH_PREFIX + "constants"
48+
GLYPHS_MATH_VARIANTS_KEY = GLYPHS_MATH_PREFIX + "variants"
49+
GLYPHS_MATH_EXTENDED_SHAPE_KEY = GLYPHS_MATH_PREFIX + "extendedShape"
50+
4651
OBJECT_LIBS_KEY = "public.objectLibs"
4752
OPENTYPE_CATEGORIES_KEY = "public.openTypeCategories"
4853
OPENTYPE_META_KEY = "public.openTypeMeta"

Lib/ufo2ft/outlineCompiler.py

+183
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
COLOR_LAYERS_KEY,
3131
COLOR_PALETTES_KEY,
3232
COLR_CLIP_BOXES_KEY,
33+
GLYPHS_MATH_CONSTANTS_KEY,
34+
GLYPHS_MATH_EXTENDED_SHAPE_KEY,
35+
GLYPHS_MATH_PREFIX,
36+
GLYPHS_MATH_VARIANTS_KEY,
3337
OPENTYPE_META_KEY,
3438
OPENTYPE_POST_UNDERLINE_POSITION_KEY,
3539
UNICODE_VARIATION_SEQUENCES_KEY,
@@ -94,6 +98,7 @@ class BaseOutlineCompiler:
9498
"vhea",
9599
"COLR",
96100
"CPAL",
101+
"MATH",
97102
"meta",
98103
]
99104
)
@@ -175,6 +180,8 @@ def compile(self):
175180
self.setupTable_CPAL()
176181
if self.meta:
177182
self.setupTable_meta()
183+
if any(key.startswith(GLYPHS_MATH_PREFIX) for key in self.ufo.lib):
184+
self.setupTable_MATH()
178185
self.setupOtherTables()
179186
if self.colorLayers and self.colrAutoClipBoxes:
180187
self._computeCOLRClipBoxes()
@@ -1050,6 +1057,181 @@ def setupTable_meta(self):
10501057
f"public.openTypeMeta '{key}' value should be bytes or a string."
10511058
)
10521059

1060+
def _bboxWidth(self, glyph):
1061+
bbox = self.glyphBoundingBoxes[glyph]
1062+
if bbox is None:
1063+
return 0
1064+
return bbox.xMax - bbox.xMin
1065+
1066+
def _bboxHeight(self, glyph):
1067+
bbox = self.glyphBoundingBoxes[glyph]
1068+
if bbox is None:
1069+
return 0
1070+
return bbox.yMax - bbox.yMin
1071+
1072+
def setupTable_MATH(self):
1073+
"""
1074+
This builds MATH table based on data in the UFO font. The data is stored
1075+
either in private font/glyph lib keys or as glyph anchors with specific
1076+
names.
1077+
1078+
The data is based on GlyphsApp MATH plugin data as written out by
1079+
glyphsLib to the UFO font.
1080+
1081+
The font lib keys are:
1082+
- com.nagwa.MATHPlugin.constants: a dictionary of MATH constants as
1083+
expected by fontTools.otlLib.builder.buildMathTable(). Example:
1084+
1085+
ufo.lib["com.nagwa.MATHPlugin.constants"] = {
1086+
"ScriptPercentScaleDown": 70,
1087+
"ScriptScriptPercentScaleDown": 60,
1088+
...
1089+
}
1090+
1091+
- com.nagwa.MATHPlugin.extendedShape: a list of glyph names that
1092+
are extended shapes. Example:
1093+
1094+
ufo.lib["com.nagwa.MATHPlugin.extendedShapes"] = ["integral", "radical"]
1095+
1096+
The glyph lib keys are:
1097+
- com.nagwa.MATHPlugin.variants: a dictionary of MATH glyph variants
1098+
keyed by glyph names, and each value is a dictionary with keys
1099+
"hVariants", "vVariants", "hAssembly", and "vAssembly". Example:
1100+
1101+
ufo["braceleft"].lib["com.nagwa.MATHPlugin.variants"] = {
1102+
"vVariants": ["braceleft", "braceleft.s1", "braceleft.s2"],
1103+
"vAssembly": [
1104+
# glyph name, flags, start connector length, end connector length
1105+
["braceleft.bottom", 0, 0, 200],
1106+
["braceleft.extender", 1, 200, 200],
1107+
["braceleft.middle", 0, 100, 100],
1108+
["braceleft.extender", 1, 200, 200],
1109+
["braceleft.top", 0, 200, 0],
1110+
],
1111+
}
1112+
1113+
The anchors are:
1114+
- math.ic: italic correction anchor
1115+
- math.ta: top accent attachment anchor
1116+
- math.tr*: top right kerning anchors
1117+
- math.tl*: top left kerning anchors
1118+
- math.br*: bottom right kerning anchors
1119+
- math.bl*: bottom left kerning anchors
1120+
"""
1121+
if "MATH" not in self.tables:
1122+
return
1123+
1124+
from fontTools.otlLib.builder import buildMathTable
1125+
1126+
ufo = self.ufo
1127+
constants = ufo.lib.get(GLYPHS_MATH_CONSTANTS_KEY)
1128+
min_connector_overlap = constants.pop("MinConnectorOverlap", 0)
1129+
1130+
italics_correction = {}
1131+
top_accent_attachment = {}
1132+
math_kerns = {}
1133+
kerning_sides = {
1134+
"tr": "TopRight",
1135+
"tl": "TopLeft",
1136+
"br": "BottomRight",
1137+
"bl": "BottomLeft",
1138+
}
1139+
for name, glyph in self.allGlyphs.items():
1140+
kerns = {}
1141+
for anchor in glyph.anchors:
1142+
if anchor.name == "math.ic":
1143+
# The anchor x position is absolute, but we want
1144+
# a value relative to the glyph's width.
1145+
italics_correction[name] = anchor.x - glyph.width
1146+
if anchor.name == "math.ta":
1147+
top_accent_attachment[name] = anchor.x
1148+
for aName in kerning_sides.keys():
1149+
if anchor.name.startswith(f"math.{aName}"):
1150+
side = kerning_sides[aName]
1151+
# The anchor x positions are absolute, but we want
1152+
# values relative to the glyph's width/origin.
1153+
x, y = anchor.x, anchor.y
1154+
if side.endswith("Right"):
1155+
x -= glyph.width
1156+
elif side.endswith("Left"):
1157+
x = -x
1158+
kerns.setdefault(side, []).append([x, y])
1159+
if kerns:
1160+
math_kerns[name] = {}
1161+
# Convert anchor positions to correction heights and kern
1162+
# values.
1163+
for side, pts in kerns.items():
1164+
pts = sorted(pts, key=lambda pt: pt[1])
1165+
# Y positions, the last one is ignored as the last kern
1166+
# value is applied to all heights greater than the last one.
1167+
correctionHeights = [pt[1] for pt in pts[:-1]]
1168+
# X positions
1169+
kernValues = [pt[0] for pt in pts]
1170+
math_kerns[name][side] = (correctionHeights, kernValues)
1171+
1172+
# buildMathTable takes two dictionaries of glyph variants, one for
1173+
# horizontal variants and one for vertical variants, and items are
1174+
# tuples of glyph name and the advance width/height of the variant.
1175+
# Here we convert the UFO data to the expected format and measure the
1176+
# advances.
1177+
h_variants = {}
1178+
v_variants = {}
1179+
# It also takes two dictionaries of glyph assemblies, one for
1180+
# horizontal assemblies and one for vertical assemblies, and items are
1181+
# lists of tuples of assembly parts and italics correction, and the
1182+
# assembly part includes the advance width/height of the part. Here we
1183+
# convert the UFO data to the expected format and measure the advances.
1184+
h_assemblies = {}
1185+
v_assemblies = {}
1186+
for name, glyph in self.allGlyphs.items():
1187+
if GLYPHS_MATH_VARIANTS_KEY in glyph.lib:
1188+
variants = glyph.lib[GLYPHS_MATH_VARIANTS_KEY]
1189+
if "hVariants" in variants:
1190+
h_variants[name] = [
1191+
(n, self._bboxWidth(n)) for n in variants["hVariants"]
1192+
]
1193+
if "vVariants" in variants:
1194+
v_variants[name] = [
1195+
(n, self._bboxHeight(n)) for n in variants["vVariants"]
1196+
]
1197+
if "hAssembly" in variants:
1198+
parts = variants["hAssembly"]
1199+
h_assemblies[name] = (
1200+
[(*part, self._bboxWidth(part[0])) for part in parts],
1201+
# If the last part has italic correction, we use it as
1202+
# the assembly's.
1203+
italics_correction.pop(parts[-1][0], 0),
1204+
)
1205+
if "vAssembly" in variants:
1206+
parts = variants["vAssembly"]
1207+
v_assemblies[name] = (
1208+
[(*part, self._bboxHeight(part[0])) for part in parts],
1209+
# If the last part has italic correction, we use it as
1210+
# the assembly's.
1211+
italics_correction.pop(parts[-1][0], 0),
1212+
)
1213+
1214+
# Collect the set of extended shapes, and if a shape has vertical
1215+
# variants, add the variants to the set.
1216+
extended_shapes = set(ufo.lib.get(GLYPHS_MATH_EXTENDED_SHAPE_KEY, []))
1217+
for name, variants in v_variants.items():
1218+
if name in extended_shapes:
1219+
extended_shapes.update(v[0] for v in variants)
1220+
1221+
buildMathTable(
1222+
self.otf,
1223+
constants=constants,
1224+
italicsCorrections=italics_correction,
1225+
topAccentAttachments=top_accent_attachment,
1226+
extendedShapes=extended_shapes,
1227+
mathKerns=math_kerns,
1228+
minConnectorOverlap=min_connector_overlap,
1229+
vertGlyphVariants=v_variants,
1230+
horizGlyphVariants=h_variants,
1231+
vertGlyphAssembly=v_assemblies,
1232+
horizGlyphAssembly=h_assemblies,
1233+
)
1234+
10531235
def setupOtherTables(self):
10541236
"""
10551237
Make the other tables. The default implementation does nothing.
@@ -1652,6 +1834,7 @@ def __init__(
16521834
self.draw = self._drawDefaultNotdef
16531835
self.drawPoints = self._drawDefaultNotdefPoints
16541836
self.reverseContour = reverseContour
1837+
self.lib = {}
16551838

16561839
def __len__(self):
16571840
if self.name == ".notdef":

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
fonttools[ufo,lxml]==4.48.1
1+
fonttools[ufo,lxml]==4.49.0
22
defcon==0.10.3
33
compreffor==0.5.5
44
booleanOperations==0.9.0

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
setup_requires=pytest_runner + wheel + ["setuptools_scm"],
3030
tests_require=["pytest>=2.8"],
3131
install_requires=[
32-
"fonttools[ufo]>=4.47.2",
32+
"fonttools[ufo]>=4.49.0",
3333
"cffsubr>=0.3.0",
3434
"booleanOperations>=0.9.0",
3535
],
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Prefix: Languagesystems
2+
languagesystem DFLT dflt;
3+
languagesystem math dflt;
4+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>ascender</key>
6+
<integer>800</integer>
7+
<key>capHeight</key>
8+
<integer>656</integer>
9+
<key>descender</key>
10+
<integer>-200</integer>
11+
<key>familyName</key>
12+
<string>Test Math Font</string>
13+
<key>italicAngle</key>
14+
<integer>0</integer>
15+
<key>openTypeHeadCreated</key>
16+
<string>2024/02/12 18:20:25</string>
17+
<key>openTypeOS2Type</key>
18+
<array>
19+
<integer>3</integer>
20+
</array>
21+
<key>postscriptBlueValues</key>
22+
<array>
23+
<integer>-18</integer>
24+
<integer>0</integer>
25+
</array>
26+
<key>postscriptOtherBlues</key>
27+
<array>
28+
<integer>-201</integer>
29+
<integer>-200</integer>
30+
</array>
31+
<key>postscriptUnderlinePosition</key>
32+
<integer>-150</integer>
33+
<key>postscriptUnderlineThickness</key>
34+
<integer>50</integer>
35+
<key>styleName</key>
36+
<string>Regular</string>
37+
<key>unitsPerEm</key>
38+
<integer>1000</integer>
39+
<key>versionMajor</key>
40+
<integer>1</integer>
41+
<key>versionMinor</key>
42+
<integer>0</integer>
43+
<key>xHeight</key>
44+
<integer>449</integer>
45+
</dict>
46+
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>parenleft</key>
6+
<string>parenleft.glif</string>
7+
<key>parenleft.bot</key>
8+
<string>parenleft.bot.glif</string>
9+
<key>parenleft.ext</key>
10+
<string>parenleft.ext.glif</string>
11+
<key>parenleft.size1</key>
12+
<string>parenleft.size1.glif</string>
13+
<key>parenleft.size2</key>
14+
<string>parenleft.size2.glif</string>
15+
<key>parenleft.size3</key>
16+
<string>parenleft.size3.glif</string>
17+
<key>parenleft.size4</key>
18+
<string>parenleft.size4.glif</string>
19+
<key>parenleft.top</key>
20+
<string>parenleft.top.glif</string>
21+
<key>parenright</key>
22+
<string>parenright.glif</string>
23+
<key>parenright.bot</key>
24+
<string>parenright.bot.glif</string>
25+
<key>parenright.ext</key>
26+
<string>parenright.ext.glif</string>
27+
<key>parenright.size1</key>
28+
<string>parenright.size1.glif</string>
29+
<key>parenright.size2</key>
30+
<string>parenright.size2.glif</string>
31+
<key>parenright.size3</key>
32+
<string>parenright.size3.glif</string>
33+
<key>parenright.size4</key>
34+
<string>parenright.size4.glif</string>
35+
<key>parenright.top</key>
36+
<string>parenright.top.glif</string>
37+
</dict>
38+
</plist>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<glyph name="parenleft.bot" format="2">
3+
<advance width="645"/>
4+
<outline>
5+
<contour>
6+
<point x="595" y="-1129" type="line"/>
7+
<point x="291" y="-729"/>
8+
<point x="136" y="-184"/>
9+
<point x="136" y="373" type="curve" smooth="yes"/>
10+
<point x="136" y="687" type="line"/>
11+
<point x="90" y="687" type="line"/>
12+
<point x="90" y="373" type="line" smooth="yes"/>
13+
<point x="90" y="-192"/>
14+
<point x="247" y="-749"/>
15+
<point x="559" y="-1157" type="curve"/>
16+
</contour>
17+
</outline>
18+
</glyph>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<glyph name="parenleft.ext" format="2">
3+
<advance width="645"/>
4+
<outline>
5+
<contour>
6+
<point x="136" y="0" type="line"/>
7+
<point x="136" y="800" type="line"/>
8+
<point x="90" y="800" type="line"/>
9+
<point x="90" y="0" type="line"/>
10+
</contour>
11+
</outline>
12+
</glyph>

0 commit comments

Comments
 (0)