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},