diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py
index c6de30554..6fb8091b0 100644
--- a/src/fontra/backends/designspace.py
+++ b/src/fontra/backends/designspace.py
@@ -93,7 +93,7 @@
# Fontra / UFO
"ascender": "openTypeVheaVertTypoAscender",
"descender": "openTypeVheaVertTypoDescender",
- "lineGap": "openTypeVheaVertTypoLineGap",
+ "lineGap": "openTypeVheaVertTypoLineGap", # TODO: this doesn't really belong here
# ("slopeRise", "openTypeVheaCaretSlopeRise"),
# ("slopeRun", "openTypeVheaCaretSlopeRun"),
# ("caretOffset", "openTypeVheaCaretOffset"),
@@ -115,7 +115,79 @@
("manufacturerURL", "openTypeNameManufacturerURL"),
("licenseDescription", "openTypeNameLicense"),
("licenseInfoURL", "openTypeNameLicenseURL"),
- ("vendorID", "vendorID"),
+ ("vendorID", "openTypeOS2VendorID"),
+]
+
+
+ufoInfoPrefix = "ufo.info."
+
+
+ufoInfoAttributesToRoundTrip = [
+ "openTypeGaspRangeRecords",
+ "openTypeHeadCreated",
+ "openTypeHeadFlags",
+ "openTypeHeadLowestRecPPEM",
+ "openTypeHheaAscender",
+ "openTypeHheaCaretOffset",
+ "openTypeHheaCaretSlopeRise",
+ "openTypeHheaCaretSlopeRun",
+ "openTypeHheaDescender",
+ "openTypeHheaLineGap",
+ "openTypeNameCompatibleFullName",
+ "openTypeNamePreferredFamilyName",
+ "openTypeNamePreferredSubfamilyName",
+ "openTypeNameRecords",
+ "openTypeNameUniqueID",
+ "openTypeNameVersion",
+ "openTypeNameWWSFamilyName",
+ "openTypeNameWWSSubfamilyName",
+ "openTypeOS2CodePageRanges",
+ "openTypeOS2FamilyClass",
+ "openTypeOS2Panose",
+ "openTypeOS2Selection",
+ "openTypeOS2StrikeoutPosition",
+ "openTypeOS2StrikeoutSize",
+ "openTypeOS2SubscriptXOffset",
+ "openTypeOS2SubscriptXSize",
+ "openTypeOS2SubscriptYOffset",
+ "openTypeOS2SubscriptYSize",
+ "openTypeOS2SuperscriptXOffset",
+ "openTypeOS2SuperscriptXSize",
+ "openTypeOS2SuperscriptYOffset",
+ "openTypeOS2SuperscriptYSize",
+ "openTypeOS2Type",
+ "openTypeOS2TypoAscender",
+ "openTypeOS2TypoDescender",
+ "openTypeOS2TypoLineGap",
+ "openTypeOS2UnicodeRanges",
+ "openTypeOS2WeightClass",
+ "openTypeOS2WidthClass",
+ "openTypeOS2WinAscent",
+ "openTypeOS2WinDescent",
+ "openTypeVheaCaretOffset",
+ "openTypeVheaCaretSlopeRise",
+ "openTypeVheaCaretSlopeRun",
+ "openTypeVheaVertTypoLineGap",
+ "postscriptBlueFuzz",
+ "postscriptBlueScale",
+ "postscriptBlueShift",
+ "postscriptBlueValues",
+ "postscriptDefaultCharacter",
+ "postscriptDefaultWidthX",
+ "postscriptFamilyBlues",
+ "postscriptFamilyOtherBlues",
+ "postscriptForceBold",
+ "postscriptIsFixedPitch",
+ "postscriptNominalWidthX",
+ "postscriptOtherBlues",
+ "postscriptSlantAngle",
+ "postscriptStemSnapH",
+ "postscriptStemSnapV",
+ "postscriptUnderlinePosition",
+ "postscriptUnderlineThickness",
+ "postscriptUniqueID",
+ "postscriptWeightName",
+ "postscriptWindowsCharacterSet",
]
@@ -1429,6 +1501,7 @@ def locationTuple(self):
return locationToTuple(self.location)
def asFontraFontSource(self, unitsPerEm: int) -> FontSource:
+ customData = {}
if self.isSparse:
lineMetricsHorizontalLayout: dict[str, LineMetric] = {}
lineMetricsVerticalLayout: dict[str, LineMetric] = {}
@@ -1459,6 +1532,11 @@ def asFontraFontSource(self, unitsPerEm: int) -> FontSource:
guidelines = unpackGuidelines(fontInfo.guidelines)
italicAngle = getattr(fontInfo, "italicAngle", 0)
+ for infoAttr in ufoInfoAttributesToRoundTrip:
+ value = getattr(fontInfo, infoAttr, None)
+ if value is not None:
+ customData[f"{ufoInfoPrefix}{infoAttr}"] = value
+
return FontSource(
name=self.name,
location=self.location,
@@ -1467,6 +1545,7 @@ def asFontraFontSource(self, unitsPerEm: int) -> FontSource:
lineMetricsVerticalLayout=lineMetricsVerticalLayout,
guidelines=guidelines,
isSparse=self.isSparse,
+ customData=customData,
)
def asFontraGlyphSource(self, localDefaultOverride=None):
@@ -1914,6 +1993,11 @@ def updateFontInfoFromFontSource(reader, fontSource):
fontInfo.guidelines = packGuidelines(fontSource.guidelines)
+ for key, value in fontSource.customData.items():
+ if key.startswith(ufoInfoPrefix):
+ infoAttr = key[len(ufoInfoPrefix) :]
+ setattr(fontInfo, infoAttr, value)
+
reader.writeInfo(fontInfo)
lib = reader.readLib()
diff --git a/src/fontra/client/core/font-controller.js b/src/fontra/client/core/font-controller.js
index 929242817..3106c6fc3 100644
--- a/src/fontra/client/core/font-controller.js
+++ b/src/fontra/client/core/font-controller.js
@@ -945,6 +945,7 @@ function ensureDenseSources(sources) {
return { value: metric.value, zone: metric.zone || 0 };
}
),
+ customData: source.customData || {},
};
});
}
diff --git a/src/fontra/client/core/font-sources-instancer.js b/src/fontra/client/core/font-sources-instancer.js
index 5e9ffd40f..6cd78db9f 100644
--- a/src/fontra/client/core/font-sources-instancer.js
+++ b/src/fontra/client/core/font-sources-instancer.js
@@ -1,6 +1,10 @@
import { DiscreteVariationModel } from "./discrete-variation-model.js";
import { LRUCache } from "./lru-cache.js";
-import { areGuidelinesCompatible, normalizeGuidelines } from "./utils.js";
+import {
+ areCustomDatasCompatible,
+ areGuidelinesCompatible,
+ normalizeGuidelines,
+} from "./utils.js";
import { locationToString, mapAxesFromUserSpaceToSourceSpace } from "./var-model.js";
export class FontSourcesInstancer {
@@ -37,6 +41,7 @@ export class FontSourcesInstancer {
get deltas() {
const guidelinesAreCompatible = areGuidelinesCompatible(this.fontSourcesList);
+ const customDatasAreCompatible = areCustomDatasCompatible(this.fontSourcesList);
const fixedSourceValues = this.fontSourcesList.map((source) => {
return {
@@ -46,6 +51,7 @@ export class FontSourcesInstancer {
guidelines: guidelinesAreCompatible
? normalizeGuidelines(source.guidelines, true)
: [],
+ customData: customDatasAreCompatible ? source.customData : {},
};
});
return this.model.getDeltas(fixedSourceValues);
diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js
index 7d3fff886..396130267 100644
--- a/src/fontra/client/core/utils.js
+++ b/src/fontra/client/core/utils.js
@@ -552,6 +552,27 @@ export function areGuidelinesCompatible(parents) {
return true;
}
+export function areCustomDatasCompatible(parents) {
+ const referenceCustomData = parents[0].customData;
+ if (!referenceCustomData) {
+ return false;
+ }
+ const referenceKeys = Object.keys(referenceCustomData).sort();
+
+ for (const parent of parents.slice(1)) {
+ const keys = Object.keys(parent.customData).sort();
+ if (keys.length !== referenceKeys.length) {
+ return false;
+ }
+ for (const [kA, kB] of zip(keys, referenceKeys)) {
+ if (kA != kB) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
const identityGuideline = { x: 0, y: 0, angle: 0 };
export function normalizeGuidelines(guidelines, resetLocked = false) {
diff --git a/src/fontra/core/instancer.py b/src/fontra/core/instancer.py
index 8fce741d6..6cc8acd80 100644
--- a/src/fontra/core/instancer.py
+++ b/src/fontra/core/instancer.py
@@ -515,9 +515,9 @@ def model(self):
@cached_property
def deltas(self):
- guidelinesAreCompatible = areGuidelinesCompatible(
- list(self.fontSourcesDense.values())
- )
+ fontSourcesList = list(self.fontSourcesDense.values())
+ guidelinesAreCompatible = areGuidelinesCompatible(fontSourcesList)
+ customDatasAreCompatible = areCustomDatasCompatible(fontSourcesList)
fixedSourceValues = [
MathWrapper(
@@ -526,6 +526,7 @@ def deltas(self):
location={},
name="",
guidelines=source.guidelines if guidelinesAreCompatible else [],
+ customData=source.customData if customDatasAreCompatible else {},
)
)
for source in self.fontSourcesDense.values()
@@ -575,6 +576,19 @@ def areGuidelinesCompatible(parents):
return True
+def areCustomDatasCompatible(parents):
+ if not parents:
+ return True # or False, doesn't matter
+
+ referenceKeys = parents[0].customData.keys()
+
+ for parent in parents[1:]:
+ if parent.customData.keys() != referenceKeys:
+ return False
+
+ return True
+
+
@dataclass
class MathWrapper:
subject: Any
diff --git a/src/fontra/workflow/merger.py b/src/fontra/workflow/merger.py
index 4b4ad0ca1..8e00ac965 100644
--- a/src/fontra/workflow/merger.py
+++ b/src/fontra/workflow/merger.py
@@ -131,6 +131,7 @@ def mapLocation(location):
| sourceB.lineMetricsHorizontalLayout,
lineMetricsVerticalLayout=sourceA.lineMetricsVerticalLayout
| sourceB.lineMetricsVerticalLayout,
+ customData=sourceA.customData | sourceB.customData,
)
return MergedSourcesInfo(
diff --git a/test-js/test-font-sources-instancer.js b/test-js/test-font-sources-instancer.js
index 2933d393b..d14c492a8 100644
--- a/test-js/test-font-sources-instancer.js
+++ b/test-js/test-font-sources-instancer.js
@@ -14,24 +14,28 @@ describe("FontSourcesInstancer Tests", () => {
location: { Weight: 400, Width: 50 },
verticalMetrics: { ascender: { value: 800 } },
guidelines: [{ name: "guide", x: 100, y: 200, angle: 0 }],
+ customData: {},
},
source2: {
name: "Bold",
location: { Weight: 900, Width: 50 },
verticalMetrics: { ascender: { value: 900 } },
guidelines: [],
+ customData: {},
},
source3: {
name: "Light Wide",
location: { Weight: 400, Width: 100 },
verticalMetrics: { ascender: { value: 850 } },
guidelines: [],
+ customData: {},
},
source4: {
name: "Bold Wide",
location: { Weight: 900, Width: 100 },
verticalMetrics: { ascender: { value: 950 } },
guidelines: [],
+ customData: {},
},
};
@@ -43,6 +47,7 @@ describe("FontSourcesInstancer Tests", () => {
location: { Weight: 400, Width: 50 },
verticalMetrics: { ascender: { value: 800 } },
guidelines: [{ name: "guide", x: 100, y: 200, angle: 0 }],
+ customData: {},
},
},
{
@@ -52,6 +57,7 @@ describe("FontSourcesInstancer Tests", () => {
location: { Weight: 400, Width: 50 },
verticalMetrics: { ascender: { value: 800 } },
guidelines: [{ name: "guide", x: 100, y: 200, angle: 0 }],
+ customData: {},
},
},
{
@@ -61,6 +67,7 @@ describe("FontSourcesInstancer Tests", () => {
location: { Weight: 900, Width: 50 },
verticalMetrics: { ascender: { value: 900 } },
guidelines: [],
+ customData: {},
},
},
{
@@ -70,6 +77,7 @@ describe("FontSourcesInstancer Tests", () => {
location: null,
verticalMetrics: { ascender: { value: 850 } },
guidelines: [],
+ customData: {},
},
},
{
@@ -79,6 +87,7 @@ describe("FontSourcesInstancer Tests", () => {
location: null,
verticalMetrics: { ascender: { value: 825 } },
guidelines: [],
+ customData: {},
},
},
{
@@ -88,6 +97,7 @@ describe("FontSourcesInstancer Tests", () => {
location: null,
verticalMetrics: { ascender: { value: 875 } },
guidelines: [],
+ customData: {},
},
},
];
diff --git a/test-py/data/mutatorsans/MutatorSansBoldCondensed.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansBoldCondensed.ufo/fontinfo.plist
index dc8ce6b9e..81e536c20 100644
--- a/test-py/data/mutatorsans/MutatorSansBoldCondensed.ufo/fontinfo.plist
+++ b/test-py/data/mutatorsans/MutatorSansBoldCondensed.ufo/fontinfo.plist
@@ -20,38 +20,6 @@
License same as MutatorMath. BSD 3-clause. [test-token: A]
openTypeOS2VendorID
LTTR
- postscriptBlueValues
-
- -16
- 0
- 500
- 516
- 800
- 816
-
- postscriptDefaultWidthX
- 500
- postscriptFamilyBlues
-
- postscriptFamilyOtherBlues
-
- postscriptFontName
- MutatorMathTest-BoldCondensed
- postscriptFullName
- MutatorMathTest BoldCondensed
- postscriptOtherBlues
-
- -216
- -200
-
- postscriptSlantAngle
- 0
- postscriptStemSnapH
-
- postscriptStemSnapV
-
- postscriptWindowsCharacterSet
- 1
styleMapFamilyName
styleMapStyleName
diff --git a/test-py/data/mutatorsans/MutatorSansBoldWide.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansBoldWide.ufo/fontinfo.plist
index ec7c068ab..71db3f951 100644
--- a/test-py/data/mutatorsans/MutatorSansBoldWide.ufo/fontinfo.plist
+++ b/test-py/data/mutatorsans/MutatorSansBoldWide.ufo/fontinfo.plist
@@ -20,38 +20,6 @@
License same as MutatorMath. BSD 3-clause. [test-token: B]
openTypeOS2VendorID
LTTR
- postscriptBlueValues
-
- -16
- 0
- 500
- 516
- 800
- 816
-
- postscriptDefaultWidthX
- 500
- postscriptFamilyBlues
-
- postscriptFamilyOtherBlues
-
- postscriptFontName
- MutatorMathTest-BoldWide
- postscriptFullName
- MutatorMathTest BoldWide
- postscriptOtherBlues
-
- -216
- -200
-
- postscriptSlantAngle
- 0
- postscriptStemSnapH
-
- postscriptStemSnapV
-
- postscriptWindowsCharacterSet
- 1
styleMapFamilyName
styleMapStyleName
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist
index cf31e2761..935e102a6 100644
--- a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist
@@ -55,40 +55,12 @@
0
openTypeNameLicense
License same as MutatorMath. BSD 3-clause. [test-token: C]
+ openTypeOS2TypoAscender
+ 700
+ openTypeOS2TypoDescender
+ -200
openTypeOS2VendorID
LTTR
- postscriptBlueValues
-
- -16
- 0
- 500
- 516
- 700
- 716
-
- postscriptDefaultWidthX
- 500
- postscriptFamilyBlues
-
- postscriptFamilyOtherBlues
-
- postscriptFontName
- MutatorMathTest-LightCondensed
- postscriptFullName
- MutatorMathTest LightCondensed
- postscriptOtherBlues
-
- -216
- -200
-
- postscriptSlantAngle
- 0
- postscriptStemSnapH
-
- postscriptStemSnapV
-
- postscriptWindowsCharacterSet
- 1
styleMapFamilyName
styleMapStyleName
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_end/layerinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_end/layerinfo.plist
new file mode 100644
index 000000000..f3d8faef1
--- /dev/null
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_end/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,1,1,0.7
+
+
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_off/layerinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_off/layerinfo.plist
new file mode 100644
index 000000000..9aafa3330
--- /dev/null
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.rotate_off/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,0.25,1,0.7
+
+
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.weight=1/layerinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.weight=1/layerinfo.plist
new file mode 100644
index 000000000..3cf39b47b
--- /dev/null
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.weight=1/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 1,0.75,0,0.7
+
+
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1,weight=1/layerinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1,weight=1/layerinfo.plist
new file mode 100644
index 000000000..321e50a89
--- /dev/null
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1,weight=1/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0,1,0.25,0.7
+
+
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1/layerinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1/layerinfo.plist
new file mode 100644
index 000000000..7e385c7f1
--- /dev/null
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs.width=1/layerinfo.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ color
+ 0.5,1,0,0.7
+
+
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs/contents.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs/contents.plist
index 7fc5b8120..061bbac43 100644
--- a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs/contents.plist
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/glyphs/contents.plist
@@ -88,6 +88,8 @@
dot.glif
em
em.glif
+ nestedcomponents
+ nestedcomponents.glif
nlitest
nlitest.glif
period
@@ -108,7 +110,5 @@
varcotest1.glif
varcotest2
varcotest2.glif
- nestedcomponents
- nestedcomponents.glif
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/lib.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/lib.plist
index 239d4f424..6553e8443 100644
--- a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/lib.plist
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/lib.plist
@@ -677,6 +677,9 @@
em
varcotest1
varcotest2
+ R.alt
+ nestedcomponents
+ nlitest
testLibItemKey
diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensedItalic.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensedItalic.ufo/fontinfo.plist
index f733af19a..cf1e35c92 100644
--- a/test-py/data/mutatorsans/MutatorSansLightCondensedItalic.ufo/fontinfo.plist
+++ b/test-py/data/mutatorsans/MutatorSansLightCondensedItalic.ufo/fontinfo.plist
@@ -4,15 +4,6 @@
guidelines
- postscriptBlueValues
-
- -20
- 0
- 500
- 516
- 700
- 716
-
unitsPerEm
1000
versionMajor
diff --git a/test-py/data/mutatorsans/MutatorSansLightWide.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansLightWide.ufo/fontinfo.plist
index 1be31e44f..f33fbee60 100644
--- a/test-py/data/mutatorsans/MutatorSansLightWide.ufo/fontinfo.plist
+++ b/test-py/data/mutatorsans/MutatorSansLightWide.ufo/fontinfo.plist
@@ -20,38 +20,6 @@
License same as MutatorMath. BSD 3-clause. [test-token: D]
openTypeOS2VendorID
LTTR
- postscriptBlueValues
-
- -16
- 0
- 500
- 516
- 700
- 716
-
- postscriptDefaultWidthX
- 500
- postscriptFamilyBlues
-
- postscriptFamilyOtherBlues
-
- postscriptFontName
- MutatorMathTest-LightWide
- postscriptFullName
- MutatorMathTest LightWide
- postscriptOtherBlues
-
- -216
- -200
-
- postscriptSlantAngle
- 0
- postscriptStemSnapH
-
- postscriptStemSnapV
-
- postscriptWindowsCharacterSet
- 1
styleMapFamilyName
styleMapStyleName
diff --git a/test-py/test_backends_designspace.py b/test-py/test_backends_designspace.py
index 9b1b1400f..544f2b8cd 100644
--- a/test-py/test_backends_designspace.py
+++ b/test-py/test_backends_designspace.py
@@ -545,6 +545,10 @@ async def test_findGlyphsThatUseGlyph(writableTestFont):
{"name": "Guideline Left", "x": 60, "angle": 90},
{"name": "Guideline Baseline Overshoot", "y": -10},
],
+ "customData": {
+ "ufo.info.openTypeOS2TypoAscender": 700,
+ "ufo.info.openTypeOS2TypoDescender": -200,
+ },
},
{
"location": {"italic": 0.0, "weight": 850.0, "width": 0.0},