diff --git a/Lib/fontTools/otlLib/builder.py b/Lib/fontTools/otlLib/builder.py index c8b14fc66..7d9b99b55 100644 --- a/Lib/fontTools/otlLib/builder.py +++ b/Lib/fontTools/otlLib/builder.py @@ -1,6 +1,7 @@ from collections import namedtuple, OrderedDict import os from fontTools.misc.fixedTools import fixedToFloat +from fontTools.misc.roundTools import otRound from fontTools import ttLib from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables.otBase import ( @@ -2906,3 +2907,201 @@ def _addName(ttFont, value, minNameID=0, windows=True, mac=True): return nameTable.addMultilingualName( names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID ) + + +def buildMathTable( + ttFont, + constants=None, + italicsCorrections=None, + topAccentAttachments=None, + extendedShapes=None, + mathKerns=None, + minConnectorOverlap=0, + vertGlyphVariants=None, + horizGlyphVariants=None, + vertGlyphAssembly=None, + horizGlyphAssembly=None, +): + glyphMap = ttFont.getReverseGlyphMap() + + ttFont["MATH"] = math = ttLib.newTable("MATH") + math.table = table = ot.MATH() + table.Version = 0x00010000 + table.populateDefaults() + + table.MathConstants = _buildMathConstants(constants) + table.MathGlyphInfo = _buildMathGlyphInfo( + glyphMap, + italicsCorrections, + topAccentAttachments, + extendedShapes, + mathKerns, + ) + table.MathVariants = _buildMathVariants( + glyphMap, + minConnectorOverlap, + vertGlyphVariants, + horizGlyphVariants, + vertGlyphAssembly, + horizGlyphAssembly, + ) + + +def _buildMathConstants(constants): + if not constants: + return None + + mathConstants = ot.MathConstants() + for conv in mathConstants.getConverters(): + value = otRound(constants.get(conv.name, 0)) + if conv.tableClass: + assert issubclass(conv.tableClass, ot.MathValueRecord) + value = _mathValueRecord(value) + setattr(mathConstants, conv.name, value) + return mathConstants + + +def _buildMathGlyphInfo( + glyphMap, + italicsCorrections, + topAccentAttachments, + extendedShapes, + mathKerns, +): + if not any([extendedShapes, italicsCorrections, topAccentAttachments, mathKerns]): + return None + + info = ot.MathGlyphInfo() + info.populateDefaults() + + if italicsCorrections: + coverage = buildCoverage(italicsCorrections.keys(), glyphMap) + info.MathItalicsCorrectionInfo = ot.MathItalicsCorrectionInfo() + info.MathItalicsCorrectionInfo.Coverage = coverage + info.MathItalicsCorrectionInfo.ItalicsCorrectionCount = len(coverage.glyphs) + info.MathItalicsCorrectionInfo.ItalicsCorrection = [ + _mathValueRecord(italicsCorrections[n]) for n in coverage.glyphs + ] + + if topAccentAttachments: + coverage = buildCoverage(topAccentAttachments.keys(), glyphMap) + info.MathTopAccentAttachment = ot.MathTopAccentAttachment() + info.MathTopAccentAttachment.TopAccentCoverage = coverage + info.MathTopAccentAttachment.TopAccentAttachmentCount = len(coverage.glyphs) + info.MathTopAccentAttachment.TopAccentAttachment = [ + _mathValueRecord(topAccentAttachments[n]) for n in coverage.glyphs + ] + + if extendedShapes: + info.ExtendedShapeCoverage = buildCoverage(extendedShapes, glyphMap) + + if mathKerns: + coverage = buildCoverage(mathKerns.keys(), glyphMap) + info.MathKernInfo = ot.MathKernInfo() + info.MathKernInfo.MathKernCoverage = coverage + info.MathKernInfo.MathKernCount = len(coverage.glyphs) + info.MathKernInfo.MathKernInfoRecords = [] + for glyph in coverage.glyphs: + record = ot.MathKernInfoRecord() + for side in {"TopRight", "TopLeft", "BottomRight", "BottomLeft"}: + if side in mathKerns[glyph]: + correctionHeights, kernValues = mathKerns[glyph][side] + assert len(correctionHeights) == len(kernValues) - 1 + kern = ot.MathKern() + kern.HeightCount = len(correctionHeights) + kern.CorrectionHeight = [ + _mathValueRecord(h) for h in correctionHeights + ] + kern.KernValue = [_mathValueRecord(v) for v in kernValues] + setattr(record, f"{side}MathKern", kern) + info.MathKernInfo.MathKernInfoRecords.append(record) + + return info + + +def _buildMathVariants( + glyphMap, + minConnectorOverlap, + vertGlyphVariants, + horizGlyphVariants, + vertGlyphAssembly, + horizGlyphAssembly, +): + if not any( + [vertGlyphVariants, horizGlyphVariants, vertGlyphAssembly, horizGlyphAssembly] + ): + return None + + variants = ot.MathVariants() + variants.populateDefaults() + + variants.MinConnectorOverlap = minConnectorOverlap + + if vertGlyphVariants or vertGlyphAssembly: + variants.VertGlyphCoverage, variants.VertGlyphConstruction = ( + _buildMathGlyphConstruction( + glyphMap, + vertGlyphVariants, + vertGlyphAssembly, + ) + ) + + if horizGlyphVariants or horizGlyphAssembly: + variants.HorizGlyphCoverage, variants.HorizGlyphConstruction = ( + _buildMathGlyphConstruction( + glyphMap, + horizGlyphVariants, + horizGlyphAssembly, + ) + ) + + return variants + + +def _buildMathGlyphConstruction(glyphMap, variants, assemblies): + glyphs = set() + if variants: + glyphs.update(variants.keys()) + if assemblies: + glyphs.update(assemblies.keys()) + coverage = buildCoverage(glyphs, glyphMap) + constructions = [] + + for glyphName in coverage.glyphs: + construction = ot.MathGlyphConstruction() + construction.populateDefaults() + + if variants and glyphName in variants: + construction.VariantCount = len(variants[glyphName]) + construction.MathGlyphVariantRecord = [] + for variantName, advance in variants[glyphName]: + record = ot.MathGlyphVariantRecord() + record.VariantGlyph = variantName + record.AdvanceMeasurement = otRound(advance) + construction.MathGlyphVariantRecord.append(record) + + if assemblies and glyphName in assemblies: + parts, ic = assemblies[glyphName] + construction.GlyphAssembly = ot.GlyphAssembly() + construction.GlyphAssembly.ItalicsCorrection = _mathValueRecord(ic) + construction.GlyphAssembly.PartCount = len(parts) + construction.GlyphAssembly.PartRecords = [] + for part in parts: + part_name, flags, start, end, advance = part + record = ot.GlyphPartRecord() + record.glyph = part_name + record.PartFlags = int(flags) + record.StartConnectorLength = otRound(start) + record.EndConnectorLength = otRound(end) + record.FullAdvance = otRound(advance) + construction.GlyphAssembly.PartRecords.append(record) + + constructions.append(construction) + + return coverage, constructions + + +def _mathValueRecord(value): + value_record = ot.MathValueRecord() + value_record.Value = otRound(value) + return value_record diff --git a/Tests/otlLib/builder_test.py b/Tests/otlLib/builder_test.py index e2743808b..0d0b213fb 100644 --- a/Tests/otlLib/builder_test.py +++ b/Tests/otlLib/builder_test.py @@ -1549,6 +1549,310 @@ def test_stat_infinities(): assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff" +def test_buildMathTable_empty(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder([]) + builder.buildMathTable(ttFont) + + assert "MATH" in ttFont + mathTable = ttFont["MATH"].table + assert mathTable.Version == 0x00010000 + + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants is None + + +def test_buildMathTable_constants(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder([]) + constants = { + "AccentBaseHeight": 516, + "AxisHeight": 262, + "DelimitedSubFormulaMinHeight": 1500, + "DisplayOperatorMinHeight": 2339, + "FlattenedAccentBaseHeight": 698, + "FractionDenomDisplayStyleGapMin": 198, + "FractionDenominatorDisplayStyleShiftDown": 698, + "FractionDenominatorGapMin": 66, + "FractionDenominatorShiftDown": 465, + "FractionNumDisplayStyleGapMin": 198, + "FractionNumeratorDisplayStyleShiftUp": 774, + "FractionNumeratorGapMin": 66, + "FractionNumeratorShiftUp": 516, + "FractionRuleThickness": 66, + "LowerLimitBaselineDropMin": 585, + "LowerLimitGapMin": 132, + "MathLeading": 300, + "OverbarExtraAscender": 66, + "OverbarRuleThickness": 66, + "OverbarVerticalGap": 198, + "RadicalDegreeBottomRaisePercent": 75, + "RadicalDisplayStyleVerticalGap": 195, + "RadicalExtraAscender": 66, + "RadicalKernAfterDegree": -556, + "RadicalKernBeforeDegree": 278, + "RadicalRuleThickness": 66, + "RadicalVerticalGap": 82, + "ScriptPercentScaleDown": 70, + "ScriptScriptPercentScaleDown": 55, + "SkewedFractionHorizontalGap": 66, + "SkewedFractionVerticalGap": 77, + "SpaceAfterScript": 42, + "StackBottomDisplayStyleShiftDown": 698, + "StackBottomShiftDown": 465, + "StackDisplayStyleGapMin": 462, + "StackGapMin": 198, + "StackTopDisplayStyleShiftUp": 774, + "StackTopShiftUp": 516, + "StretchStackBottomShiftDown": 585, + "StretchStackGapAboveMin": 132, + "StretchStackGapBelowMin": 132, + "StretchStackTopShiftUp": 165, + "SubSuperscriptGapMin": 264, + "SubscriptBaselineDropMin": 105, + "SubscriptShiftDown": 140, + "SubscriptTopMax": 413, + "SuperscriptBaselineDropMax": 221, + "SuperscriptBottomMaxWithSubscript": 413, + "SuperscriptBottomMin": 129, + "SuperscriptShiftUp": 477, + "SuperscriptShiftUpCramped": 358, + "UnderbarExtraDescender": 66, + "UnderbarRuleThickness": 66, + "UnderbarVerticalGap": 198, + "UpperLimitBaselineRiseMin": 165, + "UpperLimitGapMin": 132, + } + builder.buildMathTable(ttFont, constants=constants) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants is None + for k, v in constants.items(): + r = getattr(mathTable.MathConstants, k) + try: + r = r.Value + except AttributeError: + pass + assert r == v + + +def test_buildMathTable_italicsCorrection(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) + italicsCorrections = {"A": 100, "C": 300, "D": 400, "E": 500} + builder.buildMathTable(ttFont, italicsCorrections=italicsCorrections) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo + assert mathTable.MathVariants is None + assert set( + mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs + ) == set(italicsCorrections.keys()) + for glyph, correction in zip( + mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.Coverage.glyphs, + mathTable.MathGlyphInfo.MathItalicsCorrectionInfo.ItalicsCorrection, + ): + assert correction.Value == italicsCorrections[glyph] + + +def test_buildMathTable_topAccentAttachment(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) + topAccentAttachments = {"A": 10, "B": 20, "C": 30, "E": 50} + builder.buildMathTable(ttFont, topAccentAttachments=topAccentAttachments) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo + assert mathTable.MathVariants is None + assert set( + mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs + ) == set(topAccentAttachments.keys()) + for glyph, attachment in zip( + mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentCoverage.glyphs, + mathTable.MathGlyphInfo.MathTopAccentAttachment.TopAccentAttachment, + ): + assert attachment.Value == topAccentAttachments[glyph] + + +def test_buildMathTable_extendedShape(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]) + extendedShapes = {"A", "C", "E", "F"} + builder.buildMathTable(ttFont, extendedShapes=extendedShapes) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo + assert mathTable.MathVariants is None + assert set(mathTable.MathGlyphInfo.ExtendedShapeCoverage.glyphs) == extendedShapes + + +def test_buildMathTable_mathKern(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "B"]) + mathKerns = { + "A": { + "TopRight": ([10, 20], [10, 20, 30]), + "BottomRight": ([], [10]), + "TopLeft": ([10], [0, 20]), + "BottomLeft": ([-10, 0], [0, 10, 20]), + }, + } + builder.buildMathTable(ttFont, mathKerns=mathKerns) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo + assert mathTable.MathVariants is None + assert set(mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs) == set( + mathKerns.keys() + ) + for glyph, record in zip( + mathTable.MathGlyphInfo.MathKernInfo.MathKernCoverage.glyphs, + mathTable.MathGlyphInfo.MathKernInfo.MathKernInfoRecords, + ): + h, k = mathKerns[glyph]["TopRight"] + assert [v.Value for v in record.TopRightMathKern.CorrectionHeight] == h + assert [v.Value for v in record.TopRightMathKern.KernValue] == k + h, k = mathKerns[glyph]["BottomRight"] + assert [v.Value for v in record.BottomRightMathKern.CorrectionHeight] == h + assert [v.Value for v in record.BottomRightMathKern.KernValue] == k + h, k = mathKerns[glyph]["TopLeft"] + assert [v.Value for v in record.TopLeftMathKern.CorrectionHeight] == h + assert [v.Value for v in record.TopLeftMathKern.KernValue] == k + h, k = mathKerns[glyph]["BottomLeft"] + assert [v.Value for v in record.BottomLeftMathKern.CorrectionHeight] == h + assert [v.Value for v in record.BottomLeftMathKern.KernValue] == k + + +def test_buildMathTable_vertVariants(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "A.size1", "A.size2"]) + vertGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]} + builder.buildMathTable(ttFont, vertGlyphVariants=vertGlyphVariants) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants + assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set( + vertGlyphVariants.keys() + ) + for glyph, construction in zip( + mathTable.MathVariants.VertGlyphCoverage.glyphs, + mathTable.MathVariants.VertGlyphConstruction, + ): + assert [ + (r.VariantGlyph, r.AdvanceMeasurement) + for r in construction.MathGlyphVariantRecord + ] == vertGlyphVariants[glyph] + + +def test_buildMathTable_horizVariants(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "A.size1", "A.size2"]) + horizGlyphVariants = {"A": [("A.size1", 100), ("A.size2", 200)]} + builder.buildMathTable(ttFont, horizGlyphVariants=horizGlyphVariants) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants + assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set( + horizGlyphVariants.keys() + ) + for glyph, construction in zip( + mathTable.MathVariants.HorizGlyphCoverage.glyphs, + mathTable.MathVariants.HorizGlyphConstruction, + ): + assert [ + (r.VariantGlyph, r.AdvanceMeasurement) + for r in construction.MathGlyphVariantRecord + ] == horizGlyphVariants[glyph] + + +def test_buildMathTable_vertAssembly(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"]) + vertGlyphAssembly = { + "A": [ + [ + ("A.bottom", 0, 0, 100, 200), + ("A.extender", 1, 50, 50, 100), + ("A.middle", 0, 100, 100, 200), + ("A.extender", 1, 50, 50, 100), + ("A.top", 0, 100, 0, 200), + ], + 10, + ], + } + builder.buildMathTable(ttFont, vertGlyphAssembly=vertGlyphAssembly) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants + assert set(mathTable.MathVariants.VertGlyphCoverage.glyphs) == set( + vertGlyphAssembly.keys() + ) + for glyph, construction in zip( + mathTable.MathVariants.VertGlyphCoverage.glyphs, + mathTable.MathVariants.VertGlyphConstruction, + ): + assert [ + [ + ( + r.glyph, + r.PartFlags, + r.StartConnectorLength, + r.EndConnectorLength, + r.FullAdvance, + ) + for r in construction.GlyphAssembly.PartRecords + ], + construction.GlyphAssembly.ItalicsCorrection.Value, + ] == vertGlyphAssembly[glyph] + + +def test_buildMathTable_horizAssembly(): + ttFont = ttLib.TTFont() + ttFont.setGlyphOrder(["A", "A.top", "A.middle", "A.bottom", "A.extender"]) + horizGlyphAssembly = { + "A": [ + [ + ("A.bottom", 0, 0, 100, 200), + ("A.extender", 1, 50, 50, 100), + ("A.middle", 0, 100, 100, 200), + ("A.extender", 1, 50, 50, 100), + ("A.top", 0, 100, 0, 200), + ], + 10, + ], + } + builder.buildMathTable(ttFont, horizGlyphAssembly=horizGlyphAssembly) + mathTable = ttFont["MATH"].table + assert mathTable.MathConstants is None + assert mathTable.MathGlyphInfo is None + assert mathTable.MathVariants + assert set(mathTable.MathVariants.HorizGlyphCoverage.glyphs) == set( + horizGlyphAssembly.keys() + ) + for glyph, construction in zip( + mathTable.MathVariants.HorizGlyphCoverage.glyphs, + mathTable.MathVariants.HorizGlyphConstruction, + ): + assert [ + [ + ( + r.glyph, + r.PartFlags, + r.StartConnectorLength, + r.EndConnectorLength, + r.FullAdvance, + ) + for r in construction.GlyphAssembly.PartRecords + ], + construction.GlyphAssembly.ItalicsCorrection.Value, + ] == horizGlyphAssembly[glyph] + + class ChainContextualRulesetTest(object): def test_makeRulesets(self): font = ttLib.TTFont()