From 4efc185003db8ef9aafd01778d20c00c9d3028d2 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 12 Feb 2024 01:50:05 +0200 Subject: [PATCH 1/6] Convert MATH plugin data into more usable form in UFO MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Glyphs MATH plugin stores its data in master, glyph, and layer userData. Master and layer userData convert seamlessly to UFO font and layer lib. Glyph userData, however, have no equivalent in UFO so we store them in the font’s lib under a per-glyph key prefixed by glyphsLib prefix, but this is a bit ugly. So this change groups them under two common keys using the same prefix as the other keys. So instead of: com.schriftgestaltung.Glyphs.glyphUserData.braceright com.nagwa.MATHPlugin.extendedShape 1 com.nagwa.MATHPlugin.variants vVariants ... com.schriftgestaltung.Glyphs.glyphUserData.braceleft com.nagwa.MATHPlugin.extendedShape 1 com.nagwa.MATHPlugin.variants vVariants ... We now save: com.nagwa.MATHPlugin.extendedShape parenleft parenright .... com.nagwa.MATHPlugin.variants parenleft vVariants ... parenleft vVariants ... ... --- Lib/glyphsLib/builder/constants.py | 5 +++++ Lib/glyphsLib/builder/user_data.py | 23 ++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Lib/glyphsLib/builder/constants.py b/Lib/glyphsLib/builder/constants.py index ee41cef03..6aba4c86b 100644 --- a/Lib/glyphsLib/builder/constants.py +++ b/Lib/glyphsLib/builder/constants.py @@ -320,3 +320,8 @@ } REVERSE_LANGUAGE_MAPPING = {v: k for v, k in LANGUAGE_MAPPING.items()} + +GLYPHS_MATH_PREFIX = "com.nagwa.MATHPlugin." +GLYPHS_MATH_CONSTANTS_KEY = GLYPHS_MATH_PREFIX + "constants" +GLYPHS_MATH_VARIANTS_KEY = GLYPHS_MATH_PREFIX + "variants" +GLYPHS_MATH_EXTENDED_SHAPE_KEY = GLYPHS_MATH_PREFIX + "extendedShape" diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index ece632f3a..e8e4f56bf 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -28,6 +28,8 @@ LAYER_NAME_KEY, GLYPH_USER_DATA_KEY, NODE_USER_DATA_KEY, + GLYPHS_MATH_VARIANTS_KEY, + GLYPHS_MATH_EXTENDED_SHAPE_KEY, ) @@ -59,7 +61,18 @@ def to_ufo_master_user_data(self, ufo, master): def to_ufo_glyph_user_data(self, ufo, glyph): key = GLYPH_USER_DATA_KEY + "." + glyph.name if glyph.userData: - ufo.lib[key] = dict(glyph.userData) + # Convert MATH userData to top-level keys and group them under the same + # key so that they are in a more usable/compact form. + if GLYPHS_MATH_EXTENDED_SHAPE_KEY in glyph.userData: + ufo.lib.setdefault(GLYPHS_MATH_EXTENDED_SHAPE_KEY, []).append(glyph.name) + del glyph.userData[GLYPHS_MATH_EXTENDED_SHAPE_KEY] + if GLYPHS_MATH_VARIANTS_KEY in glyph.userData: + ufo.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {})[glyph.name] = dict( + glyph.userData[GLYPHS_MATH_VARIANTS_KEY] + ) + del glyph.userData[GLYPHS_MATH_VARIANTS_KEY] + if glyph.userData: + ufo.lib[key] = dict(glyph.userData) def to_ufo_layer_lib(self, master, ufo, ufo_layer): @@ -144,6 +157,14 @@ def to_glyphs_glyph_user_data(self, ufo, glyph): if key in ufo.lib: glyph.userData = ufo.lib[key] + key = GLYPHS_MATH_EXTENDED_SHAPE_KEY + if key in ufo.lib and glyph.name in ufo.lib[key]: + glyph.userData[key] = True + + key = GLYPHS_MATH_VARIANTS_KEY + if key in ufo.lib and glyph.name in ufo.lib[key]: + glyph.userData[key] = ufo.lib[key][glyph.name] + def to_glyphs_layer_lib(self, ufo_layer, master): user_data = {} From ba573a44ae20a84630e6fb9b7fef56676b42cb6b Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 12 Feb 2024 19:51:52 +0200 Subject: [PATCH 2/6] =?UTF-8?q?Don=E2=80=99t=20modify=20input=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lib/glyphsLib/builder/user_data.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index e8e4f56bf..9bf46b750 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -30,6 +30,7 @@ NODE_USER_DATA_KEY, GLYPHS_MATH_VARIANTS_KEY, GLYPHS_MATH_EXTENDED_SHAPE_KEY, + GLYPHS_MATH_PREFIX, ) @@ -59,20 +60,22 @@ def to_ufo_master_user_data(self, ufo, master): def to_ufo_glyph_user_data(self, ufo, glyph): - key = GLYPH_USER_DATA_KEY + "." + glyph.name - if glyph.userData: + math_data = { + k: v for k, v in glyph.userData.items() if k.startswith(GLYPHS_MATH_PREFIX) + } + if math_data: # Convert MATH userData to top-level keys and group them under the same # key so that they are in a more usable/compact form. - if GLYPHS_MATH_EXTENDED_SHAPE_KEY in glyph.userData: + if GLYPHS_MATH_EXTENDED_SHAPE_KEY in math_data: ufo.lib.setdefault(GLYPHS_MATH_EXTENDED_SHAPE_KEY, []).append(glyph.name) - del glyph.userData[GLYPHS_MATH_EXTENDED_SHAPE_KEY] - if GLYPHS_MATH_VARIANTS_KEY in glyph.userData: + if GLYPHS_MATH_VARIANTS_KEY in math_data: ufo.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {})[glyph.name] = dict( - glyph.userData[GLYPHS_MATH_VARIANTS_KEY] + math_data[GLYPHS_MATH_VARIANTS_KEY] ) - del glyph.userData[GLYPHS_MATH_VARIANTS_KEY] - if glyph.userData: - ufo.lib[key] = dict(glyph.userData) + other_data = {k: v for k, v in glyph.userData.items() if k not in math_data} + key = GLYPH_USER_DATA_KEY + "." + glyph.name + if other_data: + ufo.lib[key] = dict(other_data) def to_ufo_layer_lib(self, master, ufo, ufo_layer): From 0d7c982a13d013dc36afdc4de4ab8b6929777078 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 12 Feb 2024 21:09:55 +0200 Subject: [PATCH 3/6] Add test for math userData --- tests/builder/lib_and_user_data_test.py | 46 + tests/data/Math.glyphs | 1043 +++++++++++++++++++++++ 2 files changed, 1089 insertions(+) create mode 100644 tests/data/Math.glyphs diff --git a/tests/builder/lib_and_user_data_test.py b/tests/builder/lib_and_user_data_test.py index 37c1121d6..aba89b94e 100644 --- a/tests/builder/lib_and_user_data_test.py +++ b/tests/builder/lib_and_user_data_test.py @@ -24,6 +24,9 @@ FONT_CUSTOM_PARAM_PREFIX, UFO2FT_FEATURE_WRITERS_KEY, DEFAULT_FEATURE_WRITERS, + GLYPHS_MATH_CONSTANTS_KEY, + GLYPHS_MATH_EXTENDED_SHAPE_KEY, + GLYPHS_MATH_VARIANTS_KEY, ) from glyphsLib import to_glyphs, to_ufos, to_designspace @@ -261,6 +264,49 @@ def test_glyph_user_data_into_ufo_lib(): assert font.glyphs["a"].userData["glyphUserDataKey"] == "glyphUserDataValue" +def test_math_user_data_into_ufo_lib(datadir): + font = classes.GSFont(str(datadir.join("Math.glyphs"))) + + ufos = to_ufos(font) + + math_glyphs = ["parenleft", "parenright"] + + for ufo, master in zip(ufos, font.masters): + assert ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY] == math_glyphs + + glyphs = [font.glyphs[n] for n in math_glyphs] + assert ufo.lib[GLYPHS_MATH_VARIANTS_KEY] == { + g.name: g.userData[GLYPHS_MATH_VARIANTS_KEY] for g in glyphs + } + assert ( + ufo.lib[GLYPHS_MATH_CONSTANTS_KEY] + == master.userData[GLYPHS_MATH_CONSTANTS_KEY] + ) + for name in math_glyphs: + assert ( + ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY] + == font.glyphs[name] + .layers[master.id] + .userData[GLYPHS_MATH_VARIANTS_KEY] + ) + + font2 = to_glyphs(ufos) + for master, ufo in zip(font2.masters, ufos): + assert ( + master.userData[GLYPHS_MATH_CONSTANTS_KEY] + == ufo.lib[GLYPHS_MATH_CONSTANTS_KEY] + ) + for name in math_glyphs: + assert ( + font2.glyphs[name].userData[GLYPHS_MATH_VARIANTS_KEY] + == ufo.lib[GLYPHS_MATH_VARIANTS_KEY][name] + ) + assert ( + font2.glyphs[name].layers[master.id].userData[GLYPHS_MATH_VARIANTS_KEY] + == ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY] + ) + + def test_glif_lib_equivalent_to_layer_user_data(ufo_module): ufo = ufo_module.Font() # This glyph is in the `public.default` layer diff --git a/tests/data/Math.glyphs b/tests/data/Math.glyphs new file mode 100644 index 000000000..1fde745cf --- /dev/null +++ b/tests/data/Math.glyphs @@ -0,0 +1,1043 @@ +{ +.appVersion = "3239"; +.formatVersion = 3; +axes = ( +{ +name = Weight; +tag = wght; +} +); +customParameters = ( +{ +name = "Don't use Production Names"; +value = 1; +}, +{ +name = "Enforce Compatibility Check"; +value = 0; +}, +{ +name = "Write lastChange"; +value = 0; +}, +{ +name = "Write DisplayStrings"; +value = 0; +}, +{ +name = "Use Line Breaks"; +value = 1; +} +); +date = "2024-02-12 18:20:25 +0000"; +familyName = "Test Math Font"; +featurePrefixes = ( +{ +code = "languagesystem DFLT dflt; +languagesystem math dflt; +"; +name = Languagesystems; +} +); +fontMaster = ( +{ +axesValues = ( +400 +); +customParameters = ( +{ +name = underlinePosition; +value = -150; +} +); +id = m01; +metricValues = ( +{ +pos = 800; +}, +{ +pos = 656; +}, +{ +pos = 449; +}, +{ +over = -18; +}, +{ +over = -1; +pos = -200; +}, +{ +} +); +name = Regular; +userData = { +GSCornerRadius = 13; +GSOffsetHorizontal = 23; +GSOffsetMakeStroke = 1; +GSOffsetVertical = 23; +com.nagwa.MATHPlugin.constants = { +AccentBaseHeight = 449; +AxisHeight = 250; +DelimitedSubFormulaMinHeight = 1010; +DisplayOperatorMinHeight = 2260; +FlattenedAccentBaseHeight = 656; +FractionDenomDisplayStyleGapMin = 137; +FractionDenominatorDisplayStyleShiftDown = 685; +FractionDenominatorGapMin = 45; +FractionDenominatorShiftDown = 345; +FractionNumDisplayStyleGapMin = 137; +FractionNumeratorDisplayStyleShiftUp = 677; +FractionNumeratorGapMin = 45; +FractionNumeratorShiftUp = 394; +FractionRuleThickness = 45; +LowerLimitBaselineDropMin = 600; +LowerLimitGapMin = 166; +MinConnectorOverlap = 20; +OverbarExtraAscender = 45; +OverbarRuleThickness = 45; +OverbarVerticalGap = 137; +RadicalDegreeBottomRaisePercent = 60; +RadicalDisplayStyleVerticalGap = 158; +RadicalExtraAscender = 70; +RadicalKernAfterDegree = -400; +RadicalKernBeforeDegree = 277; +RadicalRuleThickness = 45; +RadicalVerticalGap = 57; +ScriptPercentScaleDown = 70; +ScriptScriptPercentScaleDown = 50; +SkewedFractionHorizontalGap = 400; +SkewedFractionVerticalGap = 60; +SpaceAfterScript = 50; +StackBottomDisplayStyleShiftDown = 685; +StackBottomShiftDown = 345; +StackDisplayStyleGapMin = 321; +StackGapMin = 137; +StackTopDisplayStyleShiftUp = 677; +StackTopShiftUp = 444; +StretchStackBottomShiftDown = 600; +StretchStackGapAboveMin = 111; +StretchStackGapBelowMin = 166; +StretchStackTopShiftUp = 199; +SubSuperscriptGapMin = 183; +SubscriptBaselineDropMin = 50; +SubscriptShiftDown = 149; +SubscriptTopMax = 359; +SuperscriptBaselineDropMax = 385; +SuperscriptBottomMaxWithSubscript = 359; +SuperscriptBottomMin = 112; +SuperscriptShiftUp = 362; +SuperscriptShiftUpCramped = 289; +UnderbarExtraDescender = 45; +UnderbarRuleThickness = 45; +UnderbarVerticalGap = 137; +UpperLimitBaselineRiseMin = 199; +UpperLimitGapMin = 111; +}; +}; +}, +{ +axesValues = ( +700 +); +customParameters = ( +{ +name = underlinePosition; +value = -150; +} +); +iconName = Bold; +id = "4D0E0118-C279-48A5-9553-358ED916F647"; +metricValues = ( +{ +pos = 800; +}, +{ +pos = 658; +}, +{ +pos = 450; +}, +{ +over = -19; +}, +{ +pos = -200; +}, +{ +} +); +name = Bold; +userData = { +GSCornerRadius = 75; +GSOffsetHorizontal = 40; +GSOffsetMakeStroke = 1; +GSOffsetProportional = 1; +GSOffsetVertical = 23; +com.nagwa.MATHPlugin.constants = { +AccentBaseHeight = 449; +AxisHeight = 250; +DelimitedSubFormulaMinHeight = 1010; +DisplayOperatorMinHeight = 2260; +FlattenedAccentBaseHeight = 658; +FractionDenomDisplayStyleGapMin = 137; +FractionDenominatorDisplayStyleShiftDown = 685; +FractionDenominatorGapMin = 45; +FractionDenominatorShiftDown = 345; +FractionNumDisplayStyleGapMin = 137; +FractionNumeratorDisplayStyleShiftUp = 677; +FractionNumeratorGapMin = 45; +FractionNumeratorShiftUp = 394; +FractionRuleThickness = 45; +LowerLimitBaselineDropMin = 600; +LowerLimitGapMin = 166; +MinConnectorOverlap = 20; +OverbarExtraAscender = 45; +OverbarRuleThickness = 45; +OverbarVerticalGap = 137; +RadicalDegreeBottomRaisePercent = 60; +RadicalDisplayStyleVerticalGap = 158; +RadicalExtraAscender = 70; +RadicalKernAfterDegree = -400; +RadicalKernBeforeDegree = 277; +RadicalRuleThickness = 90; +RadicalVerticalGap = 57; +ScriptPercentScaleDown = 70; +ScriptScriptPercentScaleDown = 50; +SkewedFractionHorizontalGap = 400; +SkewedFractionVerticalGap = 120; +SpaceAfterScript = 50; +StackBottomDisplayStyleShiftDown = 685; +StackBottomShiftDown = 345; +StackDisplayStyleGapMin = 321; +StackGapMin = 137; +StackTopDisplayStyleShiftUp = 677; +StackTopShiftUp = 444; +StretchStackBottomShiftDown = 600; +StretchStackGapAboveMin = 111; +StretchStackGapBelowMin = 166; +StretchStackTopShiftUp = 199; +SubSuperscriptGapMin = 183; +SubscriptBaselineDropMin = 50; +SubscriptShiftDown = 149; +SubscriptTopMax = 359; +SuperscriptBaselineDropMax = 385; +SuperscriptBottomMaxWithSubscript = 359; +SuperscriptBottomMin = 112; +SuperscriptShiftUp = 362; +SuperscriptShiftUpCramped = 289; +UnderbarExtraDescender = 45; +UnderbarRuleThickness = 45; +UnderbarVerticalGap = 137; +UpperLimitBaselineRiseMin = 199; +UpperLimitGapMin = 111; +}; +}; +} +); +glyphs = ( +{ +glyphname = parenleft; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(143,529,o), +(90,381,o), +(90,232,cs), +(90,83,o), +(143,-65,o), +(249,-173,c), +(281,-141,l), +(185,-41,o), +(136,93,o), +(136,232,cs), +(136,371,o), +(185,505,o), +(281,605,c), +(249,637,l) +); +} +); +userData = { +com.nagwa.MATHPlugin.variants = { +vAssembly = ( +( +parenleft.bot, +0, +0, +314 +), +( +parenleft.ext, +1, +630, +630 +), +( +parenleft.top, +0, +314, +0 +) +); +}; +}; +width = 331; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(145,537,o), +(90,385,o), +(90,232,cs), +(90,79,o), +(145,-73,o), +(253,-185,c), +(311,-129,l), +(217,-33,o), +(170,97,o), +(170,232,cs), +(170,367,o), +(217,497,o), +(311,593,c), +(253,649,l) +); +} +); +userData = { +com.nagwa.MATHPlugin.variants = { +vAssembly = ( +( +parenleft.bot, +0, +0, +314 +), +( +parenleft.ext, +1, +1000, +1000 +), +( +parenleft.top, +0, +314, +0 +) +); +}; +}; +width = 361; +} +); +metricLeft = "=90"; +metricRight = "=50"; +unicode = 40; +userData = { +com.nagwa.MATHPlugin.extendedShape = 1; +com.nagwa.MATHPlugin.variants = { +vVariants = ( +parenleft, +parenleft.size1, +parenleft.size2, +parenleft.size3, +parenleft.size4 +); +}; +}; +}, +{ +glyphname = parenleft.bot; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(291,-729,o), +(136,-184,o), +(136,373,cs), +(136,687,l), +(90,687,l), +(90,373,ls), +(90,-192,o), +(247,-749,o), +(559,-1157,c), +(595,-1129,l) +); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(90,365,ls), +(90,-203,o), +(248,-764,o), +(562,-1175,c), +(626,-1127,l), +(324,-730,o), +(170,-189,o), +(170,365,cs), +(170,679,l), +(90,679,l) +); +} +); +width = 676; +} +); +metricLeft = "=90"; +metricRight = "=50"; +}, +{ +glyphname = parenleft.ext; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(136,800,l), +(90,800,l), +(90,0,l), +(136,0,l) +); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(170,800,l), +(90,800,l), +(90,-200,l), +(170,-200,l) +); +} +); +width = 676; +} +); +metricLeft = "=90"; +metricWidth = "=parenleft.top"; +}, +{ +glyphname = parenleft.size1; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(171,521,o), +(90,294,o), +(90,65,cs), +(90,-164,o), +(171,-391,o), +(332,-557,c), +(366,-525,l), +(213,-367,o), +(136,-154,o), +(136,65,cs), +(136,284,o), +(213,497,o), +(366,655,c), +(332,687,l) +); +} +); +width = 416; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(173,529,o), +(90,298,o), +(90,65,cs), +(90,-168,o), +(173,-399,o), +(337,-569,c), +(395,-513,l), +(245,-359,o), +(170,-150,o), +(170,65,cs), +(170,280,o), +(245,489,o), +(395,643,c), +(337,699,l) +); +} +); +width = 445; +} +); +metricLeft = "=90"; +metricRight = "=50"; +}, +{ +glyphname = parenleft.size2; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(192,437,o), +(90,100,o), +(90,-241,cs), +(90,-582,o), +(192,-919,o), +(393,-1167,c), +(429,-1137,l), +(234,-899,o), +(136,-574,o), +(136,-241,cs), +(136,92,o), +(234,417,o), +(429,657,c), +(393,685,l) +); +} +); +width = 479; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(193,445,o), +(90,104,o), +(90,-241,cs), +(90,-586,o), +(193,-927,o), +(397,-1177,c), +(459,-1127,l), +(267,-891,o), +(170,-570,o), +(170,-241,cs), +(170,88,o), +(267,409,o), +(459,646,c), +(397,696,l) +); +} +); +width = 509; +} +); +metricLeft = "=90"; +metricRight = "=50"; +}, +{ +glyphname = parenleft.size3; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(220,354,o), +(90,-96,o), +(90,-552,cs), +(90,-1008,o), +(220,-1458,o), +(475,-1788,c), +(511,-1760,l), +(262,-1438,o), +(136,-1000,o), +(136,-552,cs), +(136,-104,o), +(262,334,o), +(511,657,c), +(475,685,l) +); +} +); +width = 561; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(221,361,o), +(90,-93,o), +(90,-552,cs), +(90,-1011,o), +(221,-1465,o), +(478,-1798,c), +(542,-1750,l), +(295,-1431,o), +(170,-997,o), +(170,-552,cs), +(170,-107,o), +(295,327,o), +(542,647,c), +(478,695,l) +); +} +); +width = 592; +} +); +metricLeft = "=90"; +metricRight = "=50"; +}, +{ +glyphname = parenleft.size4; +layers = ( +{ +layerId = m01; +shapes = ( +{ +closed = 1; +nodes = ( +(248,275,o), +(90,-282,o), +(90,-847,cs), +(90,-1412,o), +(247,-1969,o), +(559,-2377,c), +(595,-2349,l), +(291,-1949,o), +(136,-1404,o), +(136,-847,cs), +(136,-290,o), +(290,255,o), +(595,657,c), +(559,685,l) +); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +closed = 1; +nodes = ( +(248,282,o), +(90,-279,o), +(90,-847,cs), +(90,-1415,o), +(248,-1976,o), +(562,-2387,c), +(626,-2339,l), +(324,-1942,o), +(170,-1401,o), +(170,-847,cs), +(170,-293,o), +(324,248,o), +(626,647,c), +(562,695,l) +); +} +); +width = 676; +} +); +metricLeft = "=90"; +metricRight = "=50"; +}, +{ +glyphname = parenleft.top; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (0,-496); +ref = parenleft.bot; +scale = (1,-1); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (0,-496); +ref = parenleft.bot; +scale = (1,-1); +} +); +width = 676; +} +); +metricLeft = "=parenleft.bot"; +metricRight = "=parenleft.bot"; +}, +{ +glyphname = parenright; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (331,0); +ref = parenleft; +scale = (-1,1); +} +); +userData = { +com.nagwa.MATHPlugin.variants = { +vAssembly = ( +( +parenright.bot, +0, +0, +314 +), +( +parenright.ext, +1, +630, +630 +), +( +parenright.top, +0, +314, +0 +) +); +}; +}; +width = 331; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (361,0); +ref = parenleft; +scale = (-1,1); +} +); +userData = { +com.nagwa.MATHPlugin.variants = { +vAssembly = ( +( +parenright.bot, +0, +0, +314 +), +( +parenright.ext, +1, +1000, +1000 +), +( +parenright.top, +0, +314, +0 +) +); +}; +}; +width = 361; +} +); +metricLeft = "=|parenleft"; +metricRight = "=|parenleft"; +unicode = 41; +userData = { +com.nagwa.MATHPlugin.extendedShape = 1; +com.nagwa.MATHPlugin.variants = { +vVariants = ( +parenright, +parenright.size1, +parenright.size2, +parenright.size3, +parenright.size4 +); +}; +}; +}, +{ +glyphname = parenright.bot; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (645,0); +ref = parenleft.bot; +scale = (-1,1); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (676,0); +ref = parenleft.bot; +scale = (-1,1); +} +); +width = 676; +} +); +metricLeft = "=|parenleft.bot"; +metricRight = "=|parenleft.bot"; +}, +{ +glyphname = parenright.ext; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (419,0); +ref = parenleft.ext; +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (416,0); +ref = parenleft.ext; +} +); +width = 676; +} +); +metricRight = "=parenright.top"; +metricWidth = "=parenright.top"; +}, +{ +glyphname = parenright.size1; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (416,0); +ref = parenleft.size1; +scale = (-1,1); +} +); +width = 416; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (445,0); +ref = parenleft.size1; +scale = (-1,1); +} +); +width = 445; +} +); +metricLeft = "=|parenleft.alt1"; +metricRight = "=|parenleft.alt1"; +}, +{ +glyphname = parenright.size2; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (479,0); +ref = parenleft.size2; +scale = (-1,1); +} +); +width = 479; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (509,0); +ref = parenleft.size2; +scale = (-1,1); +} +); +width = 509; +} +); +metricLeft = "=|parenleft.alt2"; +metricRight = "=|parenleft.alt2"; +}, +{ +glyphname = parenright.size3; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (561,0); +ref = parenleft.size3; +scale = (-1,1); +} +); +width = 561; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (592,0); +ref = parenleft.size3; +scale = (-1,1); +} +); +width = 592; +} +); +metricLeft = "=|parenleft.alt3"; +metricRight = "=|parenleft.alt3"; +}, +{ +glyphname = parenright.size4; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (645,0); +ref = parenleft.size4; +scale = (-1,1); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (676,0); +ref = parenleft.size4; +scale = (-1,1); +} +); +width = 676; +} +); +metricLeft = "=|parenleft.alt4"; +metricRight = "=|parenleft.alt4"; +}, +{ +glyphname = parenright.top; +layers = ( +{ +layerId = m01; +shapes = ( +{ +pos = (645,0); +ref = parenleft.top; +scale = (-1,1); +} +); +width = 645; +}, +{ +layerId = "4D0E0118-C279-48A5-9553-358ED916F647"; +shapes = ( +{ +pos = (676,0); +ref = parenleft.top; +scale = (-1,1); +} +); +width = 676; +} +); +metricLeft = "=|parenleft.top"; +metricRight = "=|parenleft.top"; +} +); +instances = ( +{ +axesValues = ( +400 +); +instanceInterpolations = { +m01 = 1; +}; +name = Regular; +}, +{ +axesValues = ( +700 +); +instanceInterpolations = { +"4D0E0118-C279-48A5-9553-358ED916F647" = 1; +}; +isBold = 1; +linkStyle = Regular; +name = Bold; +weightClass = 700; +} +); +metrics = ( +{ +type = ascender; +}, +{ +type = "cap height"; +}, +{ +type = "x-height"; +}, +{ +type = baseline; +}, +{ +type = descender; +}, +{ +type = "italic angle"; +} +); +settings = { +disablesAutomaticAlignment = 1; +keepAlternatesTogether = 1; +}; +unitsPerEm = 1000; +versionMajor = 1; +versionMinor = 0; +} From 699e07cd6725afca1cf25420636feb542154d549 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Mon, 12 Feb 2024 22:23:58 +0200 Subject: [PATCH 4/6] =?UTF-8?q?Don=E2=80=99t=20write=20ufo.lib=20MATG=20gl?= =?UTF-8?q?yph=20userData=20back=20to=20master.userData?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We write it to the glyph userData where they belong. --- Lib/glyphsLib/builder/user_data.py | 3 ++- tests/builder/lib_and_user_data_test.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index 9bf46b750..3b05484e9 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -140,8 +140,9 @@ def to_glyphs_family_user_data_from_ufo(self, ufo): def to_glyphs_master_user_data(self, ufo, master): """Set the GSFontMaster userData from the UFO master-specific lib data.""" target_user_data = master.userData + special_math_keys = {GLYPHS_MATH_VARIANTS_KEY, GLYPHS_MATH_EXTENDED_SHAPE_KEY} for key, value in ufo.lib.items(): - if _user_data_has_no_special_meaning(key): + if _user_data_has_no_special_meaning(key) and key not in special_math_keys: target_user_data[key] = value # Save UFO data files diff --git a/tests/builder/lib_and_user_data_test.py b/tests/builder/lib_and_user_data_test.py index aba89b94e..aaf08993f 100644 --- a/tests/builder/lib_and_user_data_test.py +++ b/tests/builder/lib_and_user_data_test.py @@ -292,6 +292,8 @@ def test_math_user_data_into_ufo_lib(datadir): font2 = to_glyphs(ufos) for master, ufo in zip(font2.masters, ufos): + assert font2.userData[GLYPHS_MATH_EXTENDED_SHAPE_KEY] is None + assert font2.userData[GLYPHS_MATH_CONSTANTS_KEY] is None assert ( master.userData[GLYPHS_MATH_CONSTANTS_KEY] == ufo.lib[GLYPHS_MATH_CONSTANTS_KEY] From ab349495411908f0f4546f3c0a28cbbb76a65cd5 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Wed, 14 Feb 2024 04:08:45 +0200 Subject: [PATCH 5/6] Pass MATH userData in minimal mode as well --- Lib/glyphsLib/builder/glyph.py | 4 ++-- Lib/glyphsLib/builder/user_data.py | 4 ++++ tests/builder/lib_and_user_data_test.py | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Lib/glyphsLib/builder/glyph.py b/Lib/glyphsLib/builder/glyph.py index b9f60abb3..ac4616fd5 100644 --- a/Lib/glyphsLib/builder/glyph.py +++ b/Lib/glyphsLib/builder/glyph.py @@ -142,9 +142,9 @@ def to_ufo_glyph(self, ufo_glyph, layer, glyph, do_color_layers=True): # noqa: self.to_ufo_guidelines(ufo_glyph, layer) # .guidelines self.to_ufo_glyph_background(ufo_glyph, layer) # below self.to_ufo_annotations(ufo_glyph, layer) # .annotations - self.to_ufo_glyph_user_data(ufo_font, glyph) # .user_data - self.to_ufo_layer_user_data(ufo_glyph, layer) # .user_data self.to_ufo_smart_component_axes(ufo_glyph, glyph) # .components + self.to_ufo_glyph_user_data(ufo_font, glyph) # .user_data + self.to_ufo_layer_user_data(ufo_glyph, layer) # .user_data # Optimization: profiling glyphs2ufo of NotoSans-MM.glyphs (6000 glyphs) on a Mac # mini late 2014, Python 3.6.8, revealed that a whopping 17% of the time was spent diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index 3b05484e9..5993ab5f5 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -72,6 +72,8 @@ def to_ufo_glyph_user_data(self, ufo, glyph): ufo.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {})[glyph.name] = dict( math_data[GLYPHS_MATH_VARIANTS_KEY] ) + if self.minimal: + return other_data = {k: v for k, v in glyph.userData.items() if k not in math_data} key = GLYPH_USER_DATA_KEY + "." + glyph.name if other_data: @@ -101,6 +103,8 @@ def to_ufo_layer_lib(self, master, ufo, ufo_layer): def to_ufo_layer_user_data(self, ufo_glyph, layer): user_data = layer.userData for key in user_data.keys(): + if self.minimal and not key.startswith(GLYPHS_MATH_PREFIX): + continue if _user_data_has_no_special_meaning(key): ufo_glyph.lib[key] = user_data[key] diff --git a/tests/builder/lib_and_user_data_test.py b/tests/builder/lib_and_user_data_test.py index aaf08993f..e28d6dbe7 100644 --- a/tests/builder/lib_and_user_data_test.py +++ b/tests/builder/lib_and_user_data_test.py @@ -16,6 +16,8 @@ import os from collections import OrderedDict +import pytest + from fontTools.designspaceLib import DesignSpaceDocument from glyphsLib import classes from glyphsLib.types import BinaryData @@ -264,10 +266,11 @@ def test_glyph_user_data_into_ufo_lib(): assert font.glyphs["a"].userData["glyphUserDataKey"] == "glyphUserDataValue" -def test_math_user_data_into_ufo_lib(datadir): +@pytest.mark.parametrize("minimal", [True, False]) +def test_math_user_data_into_ufo_lib(datadir, minimal): font = classes.GSFont(str(datadir.join("Math.glyphs"))) - ufos = to_ufos(font) + ufos = to_ufos(font, minimal=minimal) math_glyphs = ["parenleft", "parenright"] From 2433e3ef25b6d008c21315160f15981af841d433 Mon Sep 17 00:00:00 2001 From: Khaled Hosny Date: Sat, 17 Feb 2024 03:14:31 +0200 Subject: [PATCH 6/6] Store MATH Glyphs glyph.userData in UFO glyph userData The Glyphs MATH plugin makes distinction between userData that does not change per master and stores them in glyph.userDara, and that changes per-master and stores then in layer.userData. When writing to UFO we were maintaining this distinction by keeping the former in font.lib and the layer in glyph.lib. But since a UFO file represent a single master, this distinction makes no much sense, and the UFO data looks split arbitrarily. We now keep both glyph and layer userData in glyph.lib. When converting from UFO to Glyphs if the glyph-level data is different between UFO masters, we issue a warning. --- Lib/glyphsLib/builder/glyph.py | 2 +- Lib/glyphsLib/builder/user_data.py | 46 +++++++++++++--- tests/builder/lib_and_user_data_test.py | 73 ++++++++++++++++++------- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/Lib/glyphsLib/builder/glyph.py b/Lib/glyphsLib/builder/glyph.py index ac4616fd5..967e50391 100644 --- a/Lib/glyphsLib/builder/glyph.py +++ b/Lib/glyphsLib/builder/glyph.py @@ -143,7 +143,7 @@ def to_ufo_glyph(self, ufo_glyph, layer, glyph, do_color_layers=True): # noqa: self.to_ufo_glyph_background(ufo_glyph, layer) # below self.to_ufo_annotations(ufo_glyph, layer) # .annotations self.to_ufo_smart_component_axes(ufo_glyph, glyph) # .components - self.to_ufo_glyph_user_data(ufo_font, glyph) # .user_data + self.to_ufo_glyph_user_data(ufo_font, ufo_glyph, glyph) # .user_data self.to_ufo_layer_user_data(ufo_glyph, layer) # .user_data # Optimization: profiling glyphs2ufo of NotoSans-MM.glyphs (6000 glyphs) on a Mac diff --git a/Lib/glyphsLib/builder/user_data.py b/Lib/glyphsLib/builder/user_data.py index 5993ab5f5..31f054270 100644 --- a/Lib/glyphsLib/builder/user_data.py +++ b/Lib/glyphsLib/builder/user_data.py @@ -13,6 +13,7 @@ # limitations under the License. +import logging import os import posixpath @@ -33,6 +34,8 @@ GLYPHS_MATH_PREFIX, ) +logger = logging.getLogger(__name__) + def to_designspace_family_user_data(self): if self.use_designspace: @@ -59,7 +62,7 @@ def to_ufo_master_user_data(self, ufo, master): ufo.data[filename] = bytes(data) -def to_ufo_glyph_user_data(self, ufo, glyph): +def to_ufo_glyph_user_data(self, ufo, ufo_glyph, glyph): math_data = { k: v for k, v in glyph.userData.items() if k.startswith(GLYPHS_MATH_PREFIX) } @@ -69,8 +72,8 @@ def to_ufo_glyph_user_data(self, ufo, glyph): if GLYPHS_MATH_EXTENDED_SHAPE_KEY in math_data: ufo.lib.setdefault(GLYPHS_MATH_EXTENDED_SHAPE_KEY, []).append(glyph.name) if GLYPHS_MATH_VARIANTS_KEY in math_data: - ufo.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {})[glyph.name] = dict( - math_data[GLYPHS_MATH_VARIANTS_KEY] + ufo_glyph.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {}).update( + dict(math_data[GLYPHS_MATH_VARIANTS_KEY]) ) if self.minimal: return @@ -102,11 +105,18 @@ def to_ufo_layer_lib(self, master, ufo, ufo_layer): def to_ufo_layer_user_data(self, ufo_glyph, layer): user_data = layer.userData - for key in user_data.keys(): - if self.minimal and not key.startswith(GLYPHS_MATH_PREFIX): - continue + math_data = {k: v for k, v in user_data.items() if k.startswith(GLYPHS_MATH_PREFIX)} + if math_data: + if GLYPHS_MATH_VARIANTS_KEY in math_data: + ufo_glyph.lib.setdefault(GLYPHS_MATH_VARIANTS_KEY, {}).update( + dict(math_data[GLYPHS_MATH_VARIANTS_KEY]) + ) + if self.minimal: + return + other_data = {k: v for k, v in user_data.items() if k not in math_data} + for key in other_data.keys(): if _user_data_has_no_special_meaning(key): - ufo_glyph.lib[key] = user_data[key] + ufo_glyph.lib[key] = other_data[key] def to_ufo_node_user_data(self, ufo_glyph, node, user_data: dict): @@ -197,7 +207,27 @@ def to_glyphs_layer_lib(self, ufo_layer, master): def to_glyphs_layer_user_data(self, ufo_glyph, layer): user_data = layer.userData for key, value in ufo_glyph.lib.items(): - if _user_data_has_no_special_meaning(key): + if key == GLYPHS_MATH_VARIANTS_KEY: + glyph_user_data = layer.parent.userData + for sub_key, sub_value in value.items(): + if sub_key in {"vVariants", "hVariants"}: + if key not in glyph_user_data: + glyph_user_data[key] = {} + print(sub_value, glyph_user_data[key].get(sub_key)) + if ( + sub_key in glyph_user_data[key] + and glyph_user_data[key][sub_key] != sub_value + ): + logger.warning( + f"Glyph '{layer.parent.name}' already has different " + f"'{sub_key}' in userData['{key}']. Overwriting it." + ) + glyph_user_data[key][sub_key] = sub_value + else: + if key not in user_data: + user_data[key] = {} + user_data[key][sub_key] = sub_value + elif _user_data_has_no_special_meaning(key): user_data[key] = value diff --git a/tests/builder/lib_and_user_data_test.py b/tests/builder/lib_and_user_data_test.py index e28d6dbe7..4d053f154 100644 --- a/tests/builder/lib_and_user_data_test.py +++ b/tests/builder/lib_and_user_data_test.py @@ -267,7 +267,7 @@ def test_glyph_user_data_into_ufo_lib(): @pytest.mark.parametrize("minimal", [True, False]) -def test_math_user_data_into_ufo_lib(datadir, minimal): +def test_math_user_data_into_ufo_lib(datadir, minimal, caplog): font = classes.GSFont(str(datadir.join("Math.glyphs"))) ufos = to_ufos(font, minimal=minimal) @@ -276,24 +276,28 @@ def test_math_user_data_into_ufo_lib(datadir, minimal): for ufo, master in zip(ufos, font.masters): assert ufo.lib[GLYPHS_MATH_EXTENDED_SHAPE_KEY] == math_glyphs - - glyphs = [font.glyphs[n] for n in math_glyphs] - assert ufo.lib[GLYPHS_MATH_VARIANTS_KEY] == { - g.name: g.userData[GLYPHS_MATH_VARIANTS_KEY] for g in glyphs - } assert ( ufo.lib[GLYPHS_MATH_CONSTANTS_KEY] == master.userData[GLYPHS_MATH_CONSTANTS_KEY] ) for name in math_glyphs: - assert ( - ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY] - == font.glyphs[name] - .layers[master.id] - .userData[GLYPHS_MATH_VARIANTS_KEY] - ) + for key in {"vVariants", "hVariants"}: + result = ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY].get(key) + expected = font.glyphs[name].userData[GLYPHS_MATH_VARIANTS_KEY].get(key) + assert result == expected + for key in {"vAssembly", "hAssembly"}: + result = ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY].get(key) + expected = ( + font.glyphs[name] + .layers[master.id] + .userData[GLYPHS_MATH_VARIANTS_KEY] + .get(key) + ) + assert result == expected font2 = to_glyphs(ufos) + assert "already has different 'vVariants'" not in caplog.text + for master, ufo in zip(font2.masters, ufos): assert font2.userData[GLYPHS_MATH_EXTENDED_SHAPE_KEY] is None assert font2.userData[GLYPHS_MATH_CONSTANTS_KEY] is None @@ -302,14 +306,43 @@ def test_math_user_data_into_ufo_lib(datadir, minimal): == ufo.lib[GLYPHS_MATH_CONSTANTS_KEY] ) for name in math_glyphs: - assert ( - font2.glyphs[name].userData[GLYPHS_MATH_VARIANTS_KEY] - == ufo.lib[GLYPHS_MATH_VARIANTS_KEY][name] - ) - assert ( - font2.glyphs[name].layers[master.id].userData[GLYPHS_MATH_VARIANTS_KEY] - == ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY] - ) + for key in {"vVariants", "hVariants"}: + result = font2.glyphs[name].userData[GLYPHS_MATH_VARIANTS_KEY].get(key) + expected = ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY].get(key) + assert result == expected + for key in {"vAssembly", "hAssembly"}: + result = ( + font2.glyphs[name] + .layers[master.id] + .userData[GLYPHS_MATH_VARIANTS_KEY] + .get(key) + ) + expected = ufo[name].lib[GLYPHS_MATH_VARIANTS_KEY].get(key) + assert result == expected + + +@pytest.mark.parametrize("minimal", [True, False]) +def test_math_user_data_into_ufo_lib_warn(datadir, minimal, caplog): + import copy + + font = classes.GSFont(str(datadir.join("Math.glyphs"))) + + ufos = to_ufos(font, minimal=minimal) + ufos[0]["parenleft"] = copy.deepcopy(ufos[0]["parenleft"]) + ufos[0]["parenleft"].lib[GLYPHS_MATH_VARIANTS_KEY]["vVariants"].pop() + + assert ( + ufos[0]["parenleft"].lib[GLYPHS_MATH_VARIANTS_KEY]["vVariants"] + != ufos[1]["parenleft"].lib[GLYPHS_MATH_VARIANTS_KEY]["vVariants"] + ) + + to_glyphs(ufos) + + assert any(record.levelname == "WARNING" for record in caplog.records) + assert ( + "Glyph 'parenleft' already has different 'vVariants' in " + "userData['com.nagwa.MATHPlugin.variants']. Overwriting it." in caplog.text + ) def test_glif_lib_equivalent_to_layer_user_data(ufo_module):