[otlLib] Support building MATH table

This commit is contained in:
Khaled Hosny 2024-02-14 00:17:01 +02:00
parent a7a0f41c90
commit 0f953cccd8
2 changed files with 503 additions and 0 deletions

View File

@ -1,6 +1,7 @@
from collections import namedtuple, OrderedDict from collections import namedtuple, OrderedDict
import os import os
from fontTools.misc.fixedTools import fixedToFloat from fontTools.misc.fixedTools import fixedToFloat
from fontTools.misc.roundTools import otRound
from fontTools import ttLib from fontTools import ttLib
from fontTools.ttLib.tables import otTables as ot from fontTools.ttLib.tables import otTables as ot
from fontTools.ttLib.tables.otBase import ( from fontTools.ttLib.tables.otBase import (
@ -2906,3 +2907,201 @@ def _addName(ttFont, value, minNameID=0, windows=True, mac=True):
return nameTable.addMultilingualName( return nameTable.addMultilingualName(
names, ttFont=ttFont, windows=windows, mac=mac, minNameID=minNameID 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

View File

@ -1549,6 +1549,310 @@ def test_stat_infinities():
assert struct.pack(">l", posInf) == b"\x7f\xff\xff\xff" 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): class ChainContextualRulesetTest(object):
def test_makeRulesets(self): def test_makeRulesets(self):
font = ttLib.TTFont() font = ttLib.TTFont()