|
30 | 30 | COLOR_LAYERS_KEY,
|
31 | 31 | COLOR_PALETTES_KEY,
|
32 | 32 | COLR_CLIP_BOXES_KEY,
|
| 33 | + GLYPHS_MATH_CONSTANTS_KEY, |
| 34 | + GLYPHS_MATH_EXTENDED_SHAPE_KEY, |
| 35 | + GLYPHS_MATH_PREFIX, |
| 36 | + GLYPHS_MATH_VARIANTS_KEY, |
33 | 37 | OPENTYPE_META_KEY,
|
34 | 38 | OPENTYPE_POST_UNDERLINE_POSITION_KEY,
|
35 | 39 | UNICODE_VARIATION_SEQUENCES_KEY,
|
@@ -94,6 +98,7 @@ class BaseOutlineCompiler:
|
94 | 98 | "vhea",
|
95 | 99 | "COLR",
|
96 | 100 | "CPAL",
|
| 101 | + "MATH", |
97 | 102 | "meta",
|
98 | 103 | ]
|
99 | 104 | )
|
@@ -175,6 +180,8 @@ def compile(self):
|
175 | 180 | self.setupTable_CPAL()
|
176 | 181 | if self.meta:
|
177 | 182 | self.setupTable_meta()
|
| 183 | + if any(key.startswith(GLYPHS_MATH_PREFIX) for key in self.ufo.lib): |
| 184 | + self.setupTable_MATH() |
178 | 185 | self.setupOtherTables()
|
179 | 186 | if self.colorLayers and self.colrAutoClipBoxes:
|
180 | 187 | self._computeCOLRClipBoxes()
|
@@ -1050,6 +1057,181 @@ def setupTable_meta(self):
|
1050 | 1057 | f"public.openTypeMeta '{key}' value should be bytes or a string."
|
1051 | 1058 | )
|
1052 | 1059 |
|
| 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 | + |
1053 | 1235 | def setupOtherTables(self):
|
1054 | 1236 | """
|
1055 | 1237 | Make the other tables. The default implementation does nothing.
|
@@ -1652,6 +1834,7 @@ def __init__(
|
1652 | 1834 | self.draw = self._drawDefaultNotdef
|
1653 | 1835 | self.drawPoints = self._drawDefaultNotdefPoints
|
1654 | 1836 | self.reverseContour = reverseContour
|
| 1837 | + self.lib = {} |
1655 | 1838 |
|
1656 | 1839 | def __len__(self):
|
1657 | 1840 | if self.name == ".notdef":
|
|
0 commit comments