From 1c6d395304bebe02ff030e98e8926d33b3b7b658 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 18 Feb 2025 08:55:07 +0100 Subject: [PATCH 01/33] Update font data (eg. customData openTypeHheaAscender) --- .../MutatorSansLightCondensed.ufo/fontinfo.plist | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist index 935e102a6..115abe744 100644 --- a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist +++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist @@ -53,6 +53,12 @@ italicAngle 0 + openTypeHheaAscender + 888 + openTypeHheaDescender + -222 + openTypeHheaLineGap + 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: C] openTypeOS2TypoAscender From 5556427eead38880594266a921456d5101ac7c49 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 18 Feb 2025 09:56:08 +0100 Subject: [PATCH 02/33] Revert "Update font data (eg. customData openTypeHheaAscender)" This reverts commit 98df6fb95f218d86c2b6b7e5d2bd98bc75afdda6. --- .../MutatorSansLightCondensed.ufo/fontinfo.plist | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist index 115abe744..935e102a6 100644 --- a/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist +++ b/test-py/data/mutatorsans/MutatorSansLightCondensed.ufo/fontinfo.plist @@ -53,12 +53,6 @@ italicAngle 0 - openTypeHheaAscender - 888 - openTypeHheaDescender - -222 - openTypeHheaLineGap - 0 openTypeNameLicense License same as MutatorMath. BSD 3-clause. [test-token: C] openTypeOS2TypoAscender From 59a1985bc5767bf6ea3bd77b98feb77ffacec34e Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 18 Feb 2025 12:01:20 +0100 Subject: [PATCH 03/33] First draft of customData for font sources --- src/fontra/views/fontinfo/panel-sources.js | 105 ++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index fedc2394e..c70af4331 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -552,8 +552,7 @@ class SourceBox extends HTMLElement { source.lineMetricsHorizontalLayout ), guidelines: { ...source.guidelines }, - // TODO: hhea, OS/2 line metrics, etc - // customData: { ...source.customData }, + customData: { ...source.customData }, }; } @@ -676,6 +675,21 @@ class SourceBox extends HTMLElement { }, `edit guidelines`); // TODO: translation }); + this.controllers.customData.addListener((event) => { + this.editSource((source) => { + source.customData = {}; + for (const item of event.newValue) { + const value = parseFloat(item["value"]); + // TODO: How do we handle different types of values? + if (value == NaN) { + source.customData[item["key"]] = item["value"]; + } else { + source.customData[item["key"]] = value; + } + } + }, `edit customData`); // TODO: translation + }); + this.innerHTML = ""; this.append( html.div({ class: "fontra-ui-font-info-sources-panel-header" }, [ @@ -706,6 +720,12 @@ class SourceBox extends HTMLElement { ]), buildFontGuidelineList(this.controllers.guidelines) ); + this.append( + html.div({ class: "fontra-ui-font-info-sources-panel-header" }, [ + getLabelFromKey("customData"), + ]), + buildFontCustomDataList(this.controllers.customData) + ); } } } @@ -879,6 +899,86 @@ function buildFontGuidelineList(controller) { updateRemoveButton(labelList, addRemoveButton); + return html.div({ style: "display: grid; grid-gap: 0.3em; padding-bottom: 2em;" }, [ + labelList, + addRemoveButton, + ]); +} + +function buildFontCustomDataList(controller) { + const model = controller.model; + + const makeItem = ([key, value]) => { + const item = new ObservableController({ key: key, value: value }); + item.addListener((event) => { + const newCustomData = labelList.items.map((customData) => { + return { ...customData }; + }); + model.customData = newCustomData; + }); + return item.model; + }; + + const items = Object.entries(model)?.map(makeItem) || []; + + const labelList = new UIList(); + labelList.classList.add("fontra-ui-font-info-sources-panel-guideline-list"); + labelList.style = `min-width: 12em;`; + labelList.columnDescriptions = [ + { + key: "key", + title: translate("Key"), // TODO: translation + width: "14em", + editable: true, + continuous: false, + }, + { + key: "value", + title: translate("Value"), // TODO: translation + width: "14em", + editable: true, + // formatter: OptionalNumberFormatter, + continuous: false, + }, + ]; + labelList.showHeader = true; + labelList.minHeight = "5em"; + labelList.setItems(items); + + const deleteSelectedItem = () => { + const index = labelList.getSelectedItemIndex(); + if (index === undefined) { + return; + } + const items = [...labelList.items]; + items.splice(index, 1); + labelList.setItems(items); + const newCustomData = items.map((customData) => { + console.log("customData: ", customData); + return { ...customData }; + }); + model.customData = newCustomData; + }; + + labelList.addEventListener("deleteKey", deleteSelectedItem); + + const addRemoveButton = html.createDomElement("add-remove-buttons", { + addButtonCallback: () => { + // TODO: Maybe open a dialog with a list of possible keys? + const newItem = makeItem(["openTypeHheaAscender", 800]); // ufo.info. + const newItems = [...labelList.items, newItem]; + model.customData = newItems.map((label) => { + return { ...label }; + }); + labelList.setItems(newItems); + labelList.editCell(newItems.length - 1, "key"); + }, + removeButtonCallback: deleteSelectedItem, + disableRemoveButton: true, + }); + + updateRemoveButton(labelList, addRemoveButton); + return html.div({ style: "display: grid; grid-gap: 0.3em;" }, [ labelList, addRemoveButton, @@ -968,6 +1068,7 @@ function getLabelFromKey(key) { location: translate("sources.labels.location"), lineMetricsHorizontalLayout: translate("sources.labels.line-metrics"), guidelines: translate("sidebar.user-settings.guidelines"), + customData: translate("Custom Data"), // TODO: translation }; return keyLabelMap[key] || key; } From ea32f3a5b1fafa23ca9adc2070b79ccd14286f2c Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 18 Feb 2025 16:22:49 +0100 Subject: [PATCH 04/33] minor changes to title and table width --- src/fontra/views/fontinfo/panel-sources.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index c70af4331..6de550080 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -927,17 +927,16 @@ function buildFontCustomDataList(controller) { labelList.columnDescriptions = [ { key: "key", - title: translate("Key"), // TODO: translation + title: "Key", // TODO: translation width: "14em", editable: true, continuous: false, }, { key: "value", - title: translate("Value"), // TODO: translation - width: "14em", + title: "Value", // TODO: translation + width: "10em", editable: true, - // formatter: OptionalNumberFormatter, continuous: false, }, ]; @@ -954,7 +953,6 @@ function buildFontCustomDataList(controller) { items.splice(index, 1); labelList.setItems(items); const newCustomData = items.map((customData) => { - console.log("customData: ", customData); return { ...customData }; }); model.customData = newCustomData; From 09b35621b8628460f6b2a843df6bbeb7b8092b2d Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 18 Feb 2025 16:36:59 +0100 Subject: [PATCH 05/33] Adding a customData mapping: Fontra to UFO + update data. --- src/fontra/backends/designspace.py | 150 +++++++++--------- src/fontra/views/fontinfo/panel-sources.js | 2 +- .../fonts/MutatorSans.fontra/font-data.json | 4 +- .../font-data.json | 4 +- .../font-data.json | 4 +- .../output-subset-scale.fontra/font-data.json | 4 +- test-py/test_backends_designspace.py | 4 +- 7 files changed, 85 insertions(+), 87 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 1e52316fe..b73d69ad6 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -124,76 +124,74 @@ ] -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", -] +customDataNameMapping = { + # Fontra / UFO + "gaspRangeRecords": "openTypeGaspRangeRecords", + "headCreated": "openTypeHeadCreated", + "headFlags": "openTypeHeadFlags", + "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", + "hheaAscender": "openTypeHheaAscender", + "hheaCaretOffset": "openTypeHheaCaretOffset", + "hheaCaretSlopeRise": "openTypeHheaCaretSlopeRise", + "hheaCaretSlopeRun": "openTypeHheaCaretSlopeRun", + "hheaDescender": "openTypeHheaDescender", + "hheaLineGap": "openTypeHheaLineGap", + "compatibleFullName": "openTypeNameCompatibleFullName", + "preferredFamilyName": "openTypeNamePreferredFamilyName", + "preferredSubfamilyName": "openTypeNamePreferredSubfamilyName", + "records": "openTypeNameRecords", + "uniqueID": "openTypeNameUniqueID", + "version": "openTypeNameVersion", + "WWSFamilyName": "openTypeNameWWSFamilyName", + "WWSSubfamilyName": "openTypeNameWWSSubfamilyName", + "codePageRanges": "openTypeOS2CodePageRanges", + "familyClass": "openTypeOS2FamilyClass", + "panose": "openTypeOS2Panose", + "fsSelection": "openTypeOS2Selection", + "strikeoutPosition": "openTypeOS2StrikeoutPosition", + "strikeoutSize": "openTypeOS2StrikeoutSize", + "subscriptXOffset": "openTypeOS2SubscriptXOffset", + "subscriptXSize": "openTypeOS2SubscriptXSize", + "subscriptYOffset": "openTypeOS2SubscriptYOffset", + "subscriptYSize": "openTypeOS2SubscriptYSize", + "superscriptXOffset": "openTypeOS2SuperscriptXOffset", + "superscriptXSize": "openTypeOS2SuperscriptXSize", + "superscriptYOffset": "openTypeOS2SuperscriptYOffset", + "superscriptYSize": "openTypeOS2SuperscriptYSize", + "fsType": "openTypeOS2Type", + "typoAscender": "openTypeOS2TypoAscender", + "typoDescender": "openTypeOS2TypoDescender", + "typoLineGap": "openTypeOS2TypoLineGap", + "unicodeRanges": "openTypeOS2UnicodeRanges", + "weightClass": "openTypeOS2WeightClass", + "widthClass": "openTypeOS2WidthClass", + "winAscent": "openTypeOS2WinAscent", + "winDescent": "openTypeOS2WinDescent", + "vheaCaretOffset": "openTypeVheaCaretOffset", + "vheaCaretSlopeRise": "openTypeVheaCaretSlopeRise", + "vheaCaretSlopeRun": "openTypeVheaCaretSlopeRun", + "vheaVertTypoLineGap": "openTypeVheaVertTypoLineGap", + "blueFuzz": "postscriptBlueFuzz", + "blueScale": "postscriptBlueScale", + "blueShift": "postscriptBlueShift", + "blueValues": "postscriptBlueValues", + "defaultCharacter": "postscriptDefaultCharacter", + "defaultWidthX": "postscriptDefaultWidthX", + "familyBlues": "postscriptFamilyBlues", + "familyOtherBlues": "postscriptFamilyOtherBlues", + "forceBold": "postscriptForceBold", + "isFixedPitch": "postscriptIsFixedPitch", + "nominalWidthX": "postscriptNominalWidthX", + "otherBlues": "postscriptOtherBlues", + "slantAngle": "postscriptSlantAngle", + "stemSnapH": "postscriptStemSnapH", + "stemSnapV": "postscriptStemSnapV", + "underlinePosition": "postscriptUnderlinePosition", + "underlineThickness": "postscriptUnderlineThickness", + "psUniqueID": "postscriptUniqueID", # inconsistent psXXX, because uniqueID exists already. + "weightName": "postscriptWeightName", + "windowsCharacterSet": "postscriptWindowsCharacterSet", +} class DesignspaceBackend: @@ -1619,10 +1617,10 @@ def asFontraFontSource(self, unitsPerEm: int) -> FontSource: guidelines = unpackGuidelines(fontInfo.guidelines) italicAngle = getattr(fontInfo, "italicAngle", 0) - for infoAttr in ufoInfoAttributesToRoundTrip: - value = getattr(fontInfo, infoAttr, None) + for infoAttrFontra, infoAttrUFO in customDataNameMapping.items(): + value = getattr(fontInfo, infoAttrUFO, None) if value is not None: - customData[f"{ufoInfoPrefix}{infoAttr}"] = value + customData[infoAttrFontra] = value return FontSource( name=self.name, @@ -2174,9 +2172,9 @@ 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) + ufoName = customDataNameMapping.get(key) + if ufoName is not None: + setattr(fontInfo, ufoName, value) reader.writeInfo(fontInfo) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 6de550080..dfba9fb0c 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -963,7 +963,7 @@ function buildFontCustomDataList(controller) { const addRemoveButton = html.createDomElement("add-remove-buttons", { addButtonCallback: () => { // TODO: Maybe open a dialog with a list of possible keys? - const newItem = makeItem(["openTypeHheaAscender", 800]); // ufo.info. + const newItem = makeItem(["hheaAscender", 800]); const newItems = [...labelList.items, newItem]; model.customData = newItems.map((label) => { return { ...label }; diff --git a/test-common/fonts/MutatorSans.fontra/font-data.json b/test-common/fonts/MutatorSans.fontra/font-data.json index 487d71813..52764f7a8 100644 --- a/test-common/fonts/MutatorSans.fontra/font-data.json +++ b/test-common/fonts/MutatorSans.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"ufo.info.openTypeOS2TypoAscender": 700, -"ufo.info.openTypeOS2TypoDescender": -200 +"typoAscender": 700, +"typoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json b/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json index 487d71813..52764f7a8 100644 --- a/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"ufo.info.openTypeOS2TypoAscender": 700, -"ufo.info.openTypeOS2TypoDescender": -200 +"typoAscender": 700, +"typoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json b/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json index 487d71813..52764f7a8 100644 --- a/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"ufo.info.openTypeOS2TypoAscender": 700, -"ufo.info.openTypeOS2TypoDescender": -200 +"typoAscender": 700, +"typoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-scale.fontra/font-data.json b/test-py/data/workflow/output-subset-scale.fontra/font-data.json index 487d71813..52764f7a8 100644 --- a/test-py/data/workflow/output-subset-scale.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-scale.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"ufo.info.openTypeOS2TypoAscender": 700, -"ufo.info.openTypeOS2TypoDescender": -200 +"typoAscender": 700, +"typoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/test_backends_designspace.py b/test-py/test_backends_designspace.py index ac3c5fcef..140258f20 100644 --- a/test-py/test_backends_designspace.py +++ b/test-py/test_backends_designspace.py @@ -631,8 +631,8 @@ async def test_findGlyphsThatUseGlyph(writableTestFont): {"name": "Guideline Baseline Overshoot", "y": -10}, ], "customData": { - "ufo.info.openTypeOS2TypoAscender": 700, - "ufo.info.openTypeOS2TypoDescender": -200, + "typoAscender": 700, + "typoDescender": -200, }, }, { From 1329845521e0a6f63f8bd27865c24b8d7525bcdc Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 19 Feb 2025 08:29:51 +0100 Subject: [PATCH 06/33] Delete font info custom data within UFO --- src/fontra/backends/designspace.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index b73d69ad6..f9f22d103 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -2171,11 +2171,19 @@ def updateFontInfoFromFontSource(reader, fontSource): fontInfo.guidelines = packGuidelines(fontSource.guidelines) + # set custom data for key, value in fontSource.customData.items(): ufoName = customDataNameMapping.get(key) if ufoName is not None: setattr(fontInfo, ufoName, value) + # delete custom data + for fontraName, ufoName in customDataNameMapping.items(): + if fontraName not in fontSource.customData.keys(): + value = getattr(fontInfo, ufoName, None) + if value is not None: + delattr(fontInfo, ufoName) + reader.writeInfo(fontInfo) lib = reader.readLib() From e587385ec6e7eaa659ba8570b91fdc941aebc67c Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 19 Feb 2025 17:03:43 +0100 Subject: [PATCH 07/33] Raise NotImplementedError if someone tries adding a customData attribute which has no UFO mapping --- src/fontra/backends/designspace.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index f9f22d103..e90ab01ff 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -2176,6 +2176,10 @@ def updateFontInfoFromFontSource(reader, fontSource): ufoName = customDataNameMapping.get(key) if ufoName is not None: setattr(fontInfo, ufoName, value) + else: + raise NotImplementedError( + f"The CustomData attribute '{key}' lacks a UFO equivalent" + ) # delete custom data for fontraName, ufoName in customDataNameMapping.items(): From 672ef0c217bdaa76207ba74939521348e7192668 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 19 Feb 2025 17:05:35 +0100 Subject: [PATCH 08/33] Further work on font source customData (formatters, unittests, etc.) --- src/fontra/client/core/ui-utils.js | 1 + src/fontra/client/core/utils.js | 157 +++++++++++++++++++++ src/fontra/views/fontinfo/panel-axes.js | 2 +- src/fontra/views/fontinfo/panel-sources.js | 78 +++++++--- test-js/test-utils.js | 78 ++++++++++ 5 files changed, 298 insertions(+), 18 deletions(-) diff --git a/src/fontra/client/core/ui-utils.js b/src/fontra/client/core/ui-utils.js index 4a01c7052..5886334aa 100644 --- a/src/fontra/client/core/ui-utils.js +++ b/src/fontra/client/core/ui-utils.js @@ -243,6 +243,7 @@ export const DefaultFormatter = { }, }; +// TODO: Maybe move formatters to utils.js, so we can write unittests for it. export const NumberFormatter = { toString: (value) => value.toString(), fromString: (value) => { diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index c8aede7a2..f0bda0781 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -805,3 +805,160 @@ export const friendlyHttpStatus = { 504: "Gateway Timeout", 505: "HTTP Version Not Supported", }; + +// TODO: This is a temporary solution. Need further refactoring. +export const _NumberFormatter = { + toString: (value) => value.toString(), + fromString: (value) => { + if (typeof value === "number") { + return { value: value }; + } else if (typeof value === "boolean" || !value) { + return { error: "not a number" }; + } + const number = Number(value); + if (isNaN(number)) { + return { error: "not a number" }; + } else { + return { value: number }; + } + }, +}; + +export const ArrayFormatter = { + toString: (value) => { + if (Array.isArray(value)) { + return value.toString(); + } + }, + fromString: (value) => { + const array = JSON.parse("[" + value + "]"); + if (Array.isArray(array)) { + return { value: array }; + } else { + return { error: "not an array" }; + } + }, +}; + +export const NumberArrayFormatter = { + toString: (value) => value.toString(), + fromString: (value, arrayLength) => { + const array = JSON.parse("[" + value + "]"); + if (Array.isArray(array)) { + if (arrayLength && array.length != arrayLength) { + return { error: `array length must be ${arrayLength}` }; + } + return { value: array }; + } else { + return { error: "not an array" }; + } + }, +}; + +export const PanoseArrayFormatter = { + toString: (value) => NumberArrayFormatter.toString(value), + fromString: (value) => NumberArrayFormatter.fromString(value, 10), +}; + +function getAscenderDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.ascender.value || 800; +} + +function getDescenderDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.descender.value || -200; +} + +function getDescenderWinDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.descender.value * -1 || 200; +} + +function getFamilyNameDefault(fontSource = undefined) { + return fontSource.familyName || "Family Name"; +} + +function getSubfamilyNameDefault(fontSource = undefined) { + return fontSource.name || "Subfamily Name"; +} + +function getstrikeoutPositionDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.ascender.value / 2 || 250; +} + +export const customDataNameMapping = { + // verticl metrics values + hheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, + hheaDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, + hheaLineGap: { default: () => 0, formatter: _NumberFormatter }, + typoAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, + typoDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, + typoLineGap: { default: () => 0, formatter: _NumberFormatter }, + winAscent: { default: getAscenderDefault, formatter: _NumberFormatter }, + winDescent: { default: getDescenderWinDefault, formatter: _NumberFormatter }, + underlinePosition: { default: () => -100, formatter: _NumberFormatter }, + underlineThickness: { default: () => 50, formatter: _NumberFormatter }, + strikeoutPosition: { + default: getstrikeoutPositionDefault, + formatter: _NumberFormatter, + }, + strikeoutSize: { default: () => 50, formatter: _NumberFormatter }, + // name table entries + version: { default: () => "Version 1.0" }, // Name ID 7 + preferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 + preferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 + compatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 + WWSFamilyName: { default: getFamilyNameDefault }, // Name ID 21 + WWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22 + // misc + weightClass: { default: () => 400, formatter: _NumberFormatter }, + widthClass: { default: () => 5, formatter: _NumberFormatter }, + fsSelection: { default: () => 0, formatter: _NumberFormatter }, + fsType: { default: () => 0, formatter: _NumberFormatter }, + panose: { + default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], + formatter: PanoseArrayFormatter, + }, // default: sans-serif +}; + +// TODO: Based on customDataNameMapping (designspace.py) +// "gaspRangeRecords": "openTypeGaspRangeRecords", +// "headCreated": "openTypeHeadCreated", +// "headFlags": "openTypeHeadFlags", +// "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", +// "hheaCaretOffset": "openTypeHheaCaretOffset", +// "hheaCaretSlopeRise": "openTypeHheaCaretSlopeRise", +// "hheaCaretSlopeRun": "openTypeHheaCaretSlopeRun", +// "records": "openTypeNameRecords", +// "uniqueID": "openTypeNameUniqueID", +// "codePageRanges": "openTypeOS2CodePageRanges", +// "familyClass": "openTypeOS2FamilyClass", +// "subscriptXOffset": "openTypeOS2SubscriptXOffset", +// "subscriptXSize": "openTypeOS2SubscriptXSize", +// "subscriptYOffset": "openTypeOS2SubscriptYOffset", +// "subscriptYSize": "openTypeOS2SubscriptYSize", +// "superscriptXOffset": "openTypeOS2SuperscriptXOffset", +// "superscriptXSize": "openTypeOS2SuperscriptXSize", +// "superscriptYOffset": "openTypeOS2SuperscriptYOffset", +// "superscriptYSize": "openTypeOS2SuperscriptYSize", +// "unicodeRanges": "openTypeOS2UnicodeRanges", +// "vheaCaretOffset": "openTypeVheaCaretOffset", +// "vheaCaretSlopeRise": "openTypeVheaCaretSlopeRise", +// "vheaCaretSlopeRun": "openTypeVheaCaretSlopeRun", +// "vheaVertTypoLineGap": "openTypeVheaVertTypoLineGap", +// "blueFuzz": "postscriptBlueFuzz", +// "blueScale": "postscriptBlueScale", +// "blueShift": "postscriptBlueShift", +// "blueValues": "postscriptBlueValues", +// "defaultCharacter": "postscriptDefaultCharacter", +// "defaultWidthX": "postscriptDefaultWidthX", +// "familyBlues": "postscriptFamilyBlues", +// "familyOtherBlues": "postscriptFamilyOtherBlues", +// "forceBold": "postscriptForceBold", +// "isFixedPitch": "postscriptIsFixedPitch", +// "nominalWidthX": "postscriptNominalWidthX", +// "otherBlues": "postscriptOtherBlues", +// "slantAngle": "postscriptSlantAngle", +// "stemSnapH": "postscriptStemSnapH", +// "stemSnapV": "postscriptStemSnapV", +// "psUniqueID": "postscriptUniqueID", # inconsistent psXXX, because uniqueID exists already. +// "weightName": "postscriptWeightName", +// "windowsCharacterSet": "postscriptWindowsCharacterSet", diff --git a/src/fontra/views/fontinfo/panel-axes.js b/src/fontra/views/fontinfo/panel-axes.js index bc750172d..bb72b52af 100644 --- a/src/fontra/views/fontinfo/panel-axes.js +++ b/src/fontra/views/fontinfo/panel-axes.js @@ -917,7 +917,7 @@ export function updateRemoveButton(list, buttons) { }); } -function arraysEqual(arrayA, arrayB) { +export function arraysEqual(arrayA, arrayB) { if (arrayA.length !== arrayB.length) { return false; } diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index dfba9fb0c..aacdae66f 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -5,6 +5,7 @@ import { addStyleSheet } from "../core/html-utils.js"; import { translate } from "../core/localization.js"; import { ObservableController } from "../core/observable-object.js"; import { + DefaultFormatter, NumberFormatter, OptionalNumberFormatter, checkboxListCell, @@ -13,9 +14,16 @@ import { labeledTextInput, textInput, } from "../core/ui-utils.js"; -import { arrowKeyDeltas, enumerate, modulo, range, round } from "../core/utils.js"; +import { + arrowKeyDeltas, + customDataNameMapping, + enumerate, + modulo, + range, + round, +} from "../core/utils.js"; import { UIList } from "../web-components/ui-list.js"; -import { updateRemoveButton } from "./panel-axes.js"; +import { arraysEqual, updateRemoveButton } from "./panel-axes.js"; import { BaseInfoPanel } from "./panel-base.js"; import { locationToString, @@ -513,7 +521,7 @@ addStyleSheet(` padding-bottom: 1em; } -.fontra-ui-font-info-sources-panel-guideline-list { +.fontra-ui-font-info-sources-panel-list-element { min-width: max-content; max-width: 29.5em; // 4.5 + 25 max-height: 12em; @@ -679,12 +687,11 @@ class SourceBox extends HTMLElement { this.editSource((source) => { source.customData = {}; for (const item of event.newValue) { - const value = parseFloat(item["value"]); - // TODO: How do we handle different types of values? - if (value == NaN) { - source.customData[item["key"]] = item["value"]; - } else { - source.customData[item["key"]] = value; + const key = item["key"]; + const formatter = customDataNameMapping[key]?.formatter || DefaultFormatter; + const value = formatter.fromString(item["value"]).value; + if (value !== undefined) { + source.customData[key] = value; } } }, `edit customData`); // TODO: translation @@ -724,7 +731,7 @@ class SourceBox extends HTMLElement { html.div({ class: "fontra-ui-font-info-sources-panel-header" }, [ getLabelFromKey("customData"), ]), - buildFontCustomDataList(this.controllers.customData) + buildFontCustomDataList(this.controllers.customData, this.source) ); } } @@ -806,7 +813,7 @@ function buildFontGuidelineList(controller) { const items = Object.values(model)?.map(makeItem) || []; const labelList = new UIList(); - labelList.classList.add("fontra-ui-font-info-sources-panel-guideline-list"); + labelList.classList.add("fontra-ui-font-info-sources-panel-list-element"); labelList.style = `min-width: 12em;`; labelList.columnDescriptions = [ { @@ -905,13 +912,23 @@ function buildFontGuidelineList(controller) { ]); } -function buildFontCustomDataList(controller) { +function buildFontCustomDataList(controller, fontSource) { + const customDataNames = Object.keys(customDataNameMapping); const model = controller.model; const makeItem = ([key, value]) => { const item = new ObservableController({ key: key, value: value }); item.addListener((event) => { - const newCustomData = labelList.items.map((customData) => { + const sortedItems = [...labelList.items]; + sortedItems.sort( + (a, b) => customDataNames.indexOf(a.key) - customDataNames.indexOf(b.key) + ); + + if (!arraysEqual(labelList.items, sortedItems)) { + labelList.setItems(sortedItems); + } + + const newCustomData = sortedItems.map((customData) => { return { ...customData }; }); model.customData = newCustomData; @@ -919,10 +936,14 @@ function buildFontCustomDataList(controller) { return item.model; }; - const items = Object.entries(model)?.map(makeItem) || []; + const sortedItems = Object.entries(model); + sortedItems.sort( + (a, b) => customDataNames.indexOf(a[0]) - customDataNames.indexOf(b[0]) + ); + const items = sortedItems?.map(makeItem) || []; const labelList = new UIList(); - labelList.classList.add("fontra-ui-font-info-sources-panel-guideline-list"); + labelList.classList.add("fontra-ui-font-info-sources-panel-list-element"); labelList.style = `min-width: 12em;`; labelList.columnDescriptions = [ { @@ -956,20 +977,43 @@ function buildFontCustomDataList(controller) { return { ...customData }; }); model.customData = newCustomData; + addRemoveButton.scrollIntoView({ + behavior: "auto", + block: "nearest", + inline: "nearest", + }); + labelList.setSelectedItemIndex(items.length - 1); }; labelList.addEventListener("deleteKey", deleteSelectedItem); - const addRemoveButton = html.createDomElement("add-remove-buttons", { addButtonCallback: () => { // TODO: Maybe open a dialog with a list of possible keys? - const newItem = makeItem(["hheaAscender", 800]); + const currentKeys = labelList.items.map((customData) => { + return customData.key; + }); + let nextKey = `attributeName${currentKeys.length}`; + for (const key of Object.keys(customDataNameMapping)) { + if (!currentKeys.includes(key)) { + nextKey = key; + break; + } + } + const valueDefault = customDataNameMapping[nextKey] + ? customDataNameMapping[nextKey].default(fontSource) + : ""; + const newItem = makeItem([nextKey, valueDefault]); const newItems = [...labelList.items, newItem]; model.customData = newItems.map((label) => { return { ...label }; }); labelList.setItems(newItems); labelList.editCell(newItems.length - 1, "key"); + addRemoveButton.scrollIntoView({ + behavior: "auto", + block: "nearest", + inline: "nearest", + }); }, removeButtonCallback: deleteSelectedItem, disableRemoveButton: true, diff --git a/test-js/test-utils.js b/test-js/test-utils.js index c3ea8ea21..9c3168169 100644 --- a/test-js/test-utils.js +++ b/test-js/test-utils.js @@ -1,5 +1,8 @@ import { expect } from "chai"; import { + ArrayFormatter, + NumberArrayFormatter, + _NumberFormatter, arrayExtend, bisect_right, boolInt, @@ -748,3 +751,78 @@ describe("bisect_right", () => { expect(bisect_right(testCase.a, testCase.x)).to.equal(testCase.i); }); }); + +describe("NumberFormatter", () => { + parametrize( + "NumberFormatter tests", + [ + ["1", 1], + ["11234", 11234], + ["0", 0], + ["-200", -200], + ["asdfg200", undefined], + ["", undefined], + ["test", undefined], + [undefined, undefined], + [true, undefined], + [false, undefined], + [null, undefined], + [200, 200], + [0, 0], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(_NumberFormatter.fromString(input).value).to.equal(expectedResult); + } + ); +}); + +describe("ArrayFormatter", () => { + parametrize( + "ArrayFormatter fromString tests", + [ + ["1,2,3,4", [1, 2, 3, 4]], + ["1, 2,3,4", [1, 2, 3, 4]], + ["", []], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(ArrayFormatter.fromString(input).value).to.deep.equal(expectedResult); + } + ); +}); + +describe("ArrayFormatter", () => { + parametrize( + "ArrayFormatter toString tests", + [ + [[1, 2, 3, 4], "1,2,3,4"], + [[], ""], + [true, undefined], + [new Set([1, 2, 3]), undefined], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(ArrayFormatter.toString(input)).to.deep.equal(expectedResult); + } + ); +}); + +describe("NumberArrayFormatter", () => { + parametrize( + "NumberArrayFormatter fromString tests", + [ + ["1,2,3,4", [1, 2, 3, 4], 4], + ["1, 2,3, 4", [1, 2, 3, 4], 4], + ["1, 2,3,4", undefined, 3], + ["", [], 0], + ], + (testData) => { + const [input, expectedResult, arrayLength] = testData; + console.log("arrayLength: ", arrayLength); + expect(NumberArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( + expectedResult + ); + } + ); +}); From 00b341540444bb493a25dc225cc3930a4bcf4b71 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 19 Feb 2025 17:15:02 +0100 Subject: [PATCH 09/33] Removing fsSelection and fsType for now, because of "invalid type" + don't add placeholder "attributeName" --- src/fontra/client/core/utils.js | 4 ++-- src/fontra/views/fontinfo/panel-sources.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index f0bda0781..1d3ba56de 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -911,8 +911,8 @@ export const customDataNameMapping = { // misc weightClass: { default: () => 400, formatter: _NumberFormatter }, widthClass: { default: () => 5, formatter: _NumberFormatter }, - fsSelection: { default: () => 0, formatter: _NumberFormatter }, - fsType: { default: () => 0, formatter: _NumberFormatter }, + // fsSelection: { default: () => 0, formatter: _NumberFormatter }, // invalid type: () => 0 + // fsType: { default: () => 0, formatter: _NumberFormatter }, // invalid type: () => 0 panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], formatter: PanoseArrayFormatter, diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index aacdae66f..d31c6d15a 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -688,6 +688,10 @@ class SourceBox extends HTMLElement { source.customData = {}; for (const item of event.newValue) { const key = item["key"]; + if (key === "attributeName") { + // Skip this, so people can edit this placeholder it. + continue; + } const formatter = customDataNameMapping[key]?.formatter || DefaultFormatter; const value = formatter.fromString(item["value"]).value; if (value !== undefined) { @@ -992,7 +996,7 @@ function buildFontCustomDataList(controller, fontSource) { const currentKeys = labelList.items.map((customData) => { return customData.key; }); - let nextKey = `attributeName${currentKeys.length}`; + let nextKey = "attributeName"; for (const key of Object.keys(customDataNameMapping)) { if (!currentKeys.includes(key)) { nextKey = key; From 93467e5601b7aa95f8609a1826f8ed05fded6768 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 11:25:29 +0100 Subject: [PATCH 10/33] Adding fsType and fsSelection defaults --- src/fontra/backends/designspace.py | 2 +- src/fontra/client/core/utils.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index e90ab01ff..4cd9cdd15 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -141,7 +141,7 @@ "preferredSubfamilyName": "openTypeNamePreferredSubfamilyName", "records": "openTypeNameRecords", "uniqueID": "openTypeNameUniqueID", - "version": "openTypeNameVersion", + "versionString": "openTypeNameVersion", "WWSFamilyName": "openTypeNameWWSFamilyName", "WWSSubfamilyName": "openTypeNameWWSSubfamilyName", "codePageRanges": "openTypeOS2CodePageRanges", diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index 1d3ba56de..a64d107c0 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -902,7 +902,7 @@ export const customDataNameMapping = { }, strikeoutSize: { default: () => 50, formatter: _NumberFormatter }, // name table entries - version: { default: () => "Version 1.0" }, // Name ID 7 + versionString: { default: () => "Version 1.0" }, // Name ID 7 preferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 preferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 compatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 @@ -911,8 +911,8 @@ export const customDataNameMapping = { // misc weightClass: { default: () => 400, formatter: _NumberFormatter }, widthClass: { default: () => 5, formatter: _NumberFormatter }, - // fsSelection: { default: () => 0, formatter: _NumberFormatter }, // invalid type: () => 0 - // fsType: { default: () => 0, formatter: _NumberFormatter }, // invalid type: () => 0 + fsSelection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 + fsType: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], formatter: PanoseArrayFormatter, From 5971f6031745fec021f81bf3933e049cc859ecb2 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 11:45:14 +0100 Subject: [PATCH 11/33] Add "created" customData --- src/fontra/backends/designspace.py | 2 +- src/fontra/client/core/utils.js | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 4cd9cdd15..065399b03 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -127,7 +127,7 @@ customDataNameMapping = { # Fontra / UFO "gaspRangeRecords": "openTypeGaspRangeRecords", - "headCreated": "openTypeHeadCreated", + "created": "openTypeHeadCreated", "headFlags": "openTypeHeadFlags", "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", "hheaAscender": "openTypeHheaAscender", diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index a64d107c0..ea81ab3ac 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -884,6 +884,21 @@ function getstrikeoutPositionDefault(fontSource = undefined) { return fontSource.lineMetricsHorizontalLayout.ascender.value / 2 || 250; } +function getCreatedDefault() { + // Note: UTC might differ from your local time. + const date = new Date(); + + const YYYY = date.getUTCFullYear(); + const MM = String(date.getUTCMonth() + 1).padStart(2, "0"); + const DD = String(date.getUTCDate()).padStart(2, "0"); + + const HH = String(date.getUTCHours()).padStart(2, "0"); + const mm = String(date.getUTCMinutes()).padStart(2, "0"); + const SS = String(date.getUTCSeconds()).padStart(2, "0"); + + return `${YYYY}/${MM}/${DD} ${HH}:${mm}:${SS}`; // "YYYY/MM/DD HH:MM:SS" +} + export const customDataNameMapping = { // verticl metrics values hheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, @@ -911,7 +926,8 @@ export const customDataNameMapping = { // misc weightClass: { default: () => 400, formatter: _NumberFormatter }, widthClass: { default: () => 5, formatter: _NumberFormatter }, - fsSelection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 + created: { default: getCreatedDefault }, // The timezone is UTC. + fsSelection: { default: getSubfamilyNameDefault, formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 fsType: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], @@ -921,7 +937,6 @@ export const customDataNameMapping = { // TODO: Based on customDataNameMapping (designspace.py) // "gaspRangeRecords": "openTypeGaspRangeRecords", -// "headCreated": "openTypeHeadCreated", // "headFlags": "openTypeHeadFlags", // "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", // "hheaCaretOffset": "openTypeHheaCaretOffset", From 27e6e9d710824ff080c194a9857bf469d19fde08 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 14:30:07 +0100 Subject: [PATCH 12/33] Adding GlyphsApp to UFO customData key mapping This is for discussion. --- src/fontra/backends/designspace.py | 13 ++++ src/fontra/client/core/utils.js | 105 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 065399b03..838d8491f 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -62,6 +62,9 @@ from .filewatcher import Change, FileWatcher from .ufo_utils import extractGlyphNameAndCodePoints +# from glyphsLib.builder.custom_params import KNOWN_PARAM_HANDLERS + + logger = logging.getLogger(__name__) @@ -123,6 +126,16 @@ ("vendorID", "openTypeOS2VendorID"), ] +# TODO: Alternative idea: Use glyphsLib KNOWN_PARAM_HANDLERS to create the mapping +# customDataNameMapping = {} +# for handler in KNOWN_PARAM_HANDLERS: +# try: +# key = handler.glyphs_name +# except AttributeError: +# # Handler without glyphs_name +# continue +# # print(f"{key}: {handler.ufo_name}") +# customDataNameMapping[key] = handler.ufo_name customDataNameMapping = { # Fontra / UFO diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index ea81ab3ac..26e2c96a0 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -977,3 +977,108 @@ export const customDataNameMapping = { // "psUniqueID": "postscriptUniqueID", # inconsistent psXXX, because uniqueID exists already. // "weightName": "postscriptWeightName", // "windowsCharacterSet": "postscriptWindowsCharacterSet", + +// GlyphsApp to UFO mapping +// compatibleFullName: openTypeNameCompatibleFullName +// hheaAscender: openTypeHheaAscender +// hheaDescender: openTypeHheaDescender +// hheaLineGap: openTypeHheaLineGap +// panose: openTypeOS2Panose +// fsType: openTypeOS2Type +// typoAscender: openTypeOS2TypoAscender +// typoDescender: openTypeOS2TypoDescender +// typoLineGap: openTypeOS2TypoLineGap +// unicodeRanges: openTypeOS2UnicodeRanges +// strikeoutSize: openTypeOS2StrikeoutSize +// strikeoutPosition: openTypeOS2StrikeoutPosition +// subscriptXSize: openTypeOS2SubscriptXSize +// subscriptYSize: openTypeOS2SubscriptYSize +// subscriptXOffset: openTypeOS2SubscriptXOffset +// subscriptYOffset: openTypeOS2SubscriptYOffset +// superscriptXSize: openTypeOS2SuperscriptXSize +// superscriptYSize: openTypeOS2SuperscriptYSize +// superscriptXOffset: openTypeOS2SuperscriptXOffset +// superscriptYOffset: openTypeOS2SuperscriptYOffset +// vheaVertAscender: openTypeVheaVertTypoAscender +// vheaVertDescender: openTypeVheaVertTypoDescender +// vheaVertLineGap: openTypeVheaVertTypoLineGap +// vheaVertTypoAscender: openTypeVheaVertTypoAscender +// vheaVertTypoDescender: openTypeVheaVertTypoDescender +// vheaVertTypoLineGap: openTypeVheaVertTypoLineGap +// blueScale: postscriptBlueScale +// blueShift: postscriptBlueShift +// isFixedPitch: postscriptIsFixedPitch +// underlinePosition: postscriptUnderlinePosition +// underlineThickness: postscriptUnderlineThickness +// versionString: openTypeNameVersion +// vendorID: openTypeOS2VendorID +// uniqueID: openTypeNameUniqueID +// license: openTypeNameLicense +// licenseURL: openTypeNameLicenseURL +// trademark: trademark +// description: openTypeNameDescription +// sampleText: openTypeNameSampleText +// postscriptFullName: postscriptFullName +// postscriptFontName: postscriptFontName +// preferredFamilyName: openTypeNamePreferredFamilyName +// preferredSubfamilyName: openTypeNamePreferredSubfamilyName +// WWSFamilyName: openTypeNameWWSFamilyName +// WWSSubfamilyName: openTypeNameWWSSubfamilyName +// openTypeHheaCaretSlopeRun: openTypeHheaCaretSlopeRun +// openTypeVheaCaretSlopeRun: openTypeVheaCaretSlopeRun +// openTypeHheaCaretSlopeRise: openTypeHheaCaretSlopeRise +// openTypeVheaCaretSlopeRise: openTypeVheaCaretSlopeRise +// openTypeHheaCaretOffset: openTypeHheaCaretOffset +// openTypeVheaCaretOffset: openTypeVheaCaretOffset +// openTypeHeadLowestRecPPEM: openTypeHeadLowestRecPPEM +// openTypeHeadFlags: openTypeHeadFlags +// openTypeNameVersion: openTypeNameVersion +// openTypeNameUniqueID: openTypeNameUniqueID +// openTypeOS2FamilyClass: openTypeOS2FamilyClass +// postscriptSlantAngle: postscriptSlantAngle +// postscriptUniqueID: postscriptUniqueID +// postscriptBlueFuzz: postscriptBlueFuzz +// postscriptForceBold: postscriptForceBold +// postscriptDefaultWidthX: postscriptDefaultWidthX +// postscriptNominalWidthX: postscriptNominalWidthX +// postscriptWeightName: postscriptWeightName +// postscriptDefaultCharacter: postscriptDefaultCharacter +// postscriptWindowsCharacterSet: postscriptWindowsCharacterSet +// macintoshFONDFamilyID: macintoshFONDFamilyID +// macintoshFONDName: macintoshFONDName +// styleMapFamilyName: styleMapFamilyName +// styleMapStyleName: styleMapStyleName +// postscriptFamilyBlues: postscriptFamilyBlues +// postscriptFamilyOtherBlues: postscriptFamilyOtherBlues +// winAscent: openTypeOS2WinAscent +// winDescent: openTypeOS2WinDescent +// weightClass: openTypeOS2WeightClass +// widthClass: openTypeOS2WidthClass +// GASP Table: openTypeGaspRangeRecords +// gasp Table: openTypeGaspRangeRecords +// meta Table: public.openTypeMeta +// Color Palettes: com.github.googlei18n.ufo2ft.colorPalettes +// Disable Last Change: disablesLastChange +// Don't use Production Names: com.github.googlei18n.ufo2ft.useProductionNames +// disablesAutomaticAlignment: disablesAutomaticAlignment +// iconName: iconName +// DisplayStrings: DisplayStrings +// disablesNiceNames: useNiceNames +// customValue: customValue +// customValue1: customValue1 +// customValue2: customValue2 +// customValue3: customValue3 +// weightValue: weightValue +// widthValue: widthValue +// Virtual Master: Virtual Master + +// Handler without glyphs_name +// +// +// +// +// +// +// +// +// From 183c37b0cb576990bf0b887b62ad656a252a8f78 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 15:57:12 +0100 Subject: [PATCH 13/33] fix fsSelection --- src/fontra/client/core/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index 26e2c96a0..725fd8d82 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -927,7 +927,7 @@ export const customDataNameMapping = { weightClass: { default: () => 400, formatter: _NumberFormatter }, widthClass: { default: () => 5, formatter: _NumberFormatter }, created: { default: getCreatedDefault }, // The timezone is UTC. - fsSelection: { default: getSubfamilyNameDefault, formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 + fsSelection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 fsType: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], From c0fa08c8229d5361a32fb2a256a20b399e1fa487 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 16:47:05 +0100 Subject: [PATCH 14/33] Add BooleanFormatter + unittest + extend customDataNameMapping --- src/fontra/client/core/utils.js | 30 ++++++++++++++++++++++++++++++ test-js/test-utils.js | 26 +++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index 725fd8d82..8bf4f10a1 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -824,6 +824,22 @@ export const _NumberFormatter = { }, }; +export const BooleanFormatter = { + toString: (value) => value.toString(), + fromString: (value) => { + if (typeof value === "boolean") { + return { value: value }; + } + if (value.trim().toLowerCase() === "true") { + return { value: true }; + } else if (value.trim().toLowerCase() === "false") { + return { value: false }; + } else { + return { error: "not a boolean" }; + } + }, +}; + export const ArrayFormatter = { toString: (value) => { if (Array.isArray(value)) { @@ -917,6 +933,7 @@ export const customDataNameMapping = { }, strikeoutSize: { default: () => 50, formatter: _NumberFormatter }, // name table entries + uniqueID: { default: () => "uniqueID Name ID 3" }, // Name ID 3 versionString: { default: () => "Version 1.0" }, // Name ID 7 preferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 preferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 @@ -933,6 +950,19 @@ export const customDataNameMapping = { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], formatter: PanoseArrayFormatter, }, // default: sans-serif + unicodeRanges: { default: () => [], formatter: NumberArrayFormatter }, + codePageRanges: { default: () => [], formatter: NumberArrayFormatter }, + // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf + blueValues: { default: () => [], formatter: NumberArrayFormatter }, + otherBlues: { default: () => [], formatter: NumberArrayFormatter }, + familyBlues: { default: () => [], formatter: NumberArrayFormatter }, + familyOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, + blueScale: { default: () => 0.039625, formatter: _NumberFormatter }, + blueShift: { default: () => 1, formatter: _NumberFormatter }, + blueFuzz: { default: () => 1, formatter: _NumberFormatter }, + stemSnapH: { default: () => [], formatter: NumberArrayFormatter }, + stemSnapV: { default: () => [], formatter: NumberArrayFormatter }, + forceBold: { default: () => false, formatter: BooleanFormatter }, }; // TODO: Based on customDataNameMapping (designspace.py) diff --git a/test-js/test-utils.js b/test-js/test-utils.js index 9c3168169..f30bcc0ab 100644 --- a/test-js/test-utils.js +++ b/test-js/test-utils.js @@ -1,6 +1,7 @@ import { expect } from "chai"; import { ArrayFormatter, + BooleanFormatter, NumberArrayFormatter, _NumberFormatter, arrayExtend, @@ -819,10 +820,33 @@ describe("NumberArrayFormatter", () => { ], (testData) => { const [input, expectedResult, arrayLength] = testData; - console.log("arrayLength: ", arrayLength); expect(NumberArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( expectedResult ); } ); }); + +describe("BooleanFormatter", () => { + parametrize( + "BooleanFormatter fromString tests", + [ + ["false", false], + ["true", true], + ["False", false], + ["True", true], + ["FALSE", false], + ["TRUE", true], + [false, false], + [true, true], + ["", undefined], + ["Hello", undefined], + [" false ", false], + [" true ", true], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(BooleanFormatter.fromString(input).value).to.deep.equal(expectedResult); + } + ); +}); From faf3105c2a233bee19ff145f7cf535b7f1d3fb75 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 16:47:44 +0100 Subject: [PATCH 15/33] Sort unknown customData to the end of the list --- src/fontra/views/fontinfo/panel-sources.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index d31c6d15a..002c89080 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -925,7 +925,13 @@ function buildFontCustomDataList(controller, fontSource) { item.addListener((event) => { const sortedItems = [...labelList.items]; sortedItems.sort( - (a, b) => customDataNames.indexOf(a.key) - customDataNames.indexOf(b.key) + (a, b) => + (customDataNames.indexOf(a.key) != -1 + ? customDataNames.indexOf(a.key) + : customDataNames.length) - + (customDataNames.indexOf(b.key) != -1 + ? customDataNames.indexOf(b.key) + : customDataNames.length) ); if (!arraysEqual(labelList.items, sortedItems)) { @@ -942,7 +948,13 @@ function buildFontCustomDataList(controller, fontSource) { const sortedItems = Object.entries(model); sortedItems.sort( - (a, b) => customDataNames.indexOf(a[0]) - customDataNames.indexOf(b[0]) + (a, b) => + (customDataNames.indexOf(a[0]) != -1 + ? customDataNames.indexOf(a[0]) + : customDataNames.length) - + (customDataNames.indexOf(b[0]) != -1 + ? customDataNames.indexOf(b[0]) + : customDataNames.length) ); const items = sortedItems?.map(makeItem) || []; From b8bf6f0ff2093c18cb2b782cb54006c4e6f9a5c8 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Thu, 20 Feb 2025 17:18:27 +0100 Subject: [PATCH 16/33] Raise error if wrong value entered. --- src/fontra/views/fontinfo/panel-sources.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 002c89080..5a7ae0510 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -696,6 +696,11 @@ class SourceBox extends HTMLElement { const value = formatter.fromString(item["value"]).value; if (value !== undefined) { source.customData[key] = value; + } else { + message( + translate("sources.dialog.cannot-edit-source.title"), + `"${key}" invalid value: ${item["value"]}` + ); } } }, `edit customData`); // TODO: translation From 0a03e1173c6bd6876514c5b38f676a6813a41c20 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 10:25:08 +0100 Subject: [PATCH 17/33] Create formatters.js and move code. --- src/fontra/client/core/formatters.js | 72 ++++++++++++++++++ src/fontra/client/core/ui-utils.js | 2 +- src/fontra/client/core/utils.js | 76 ++----------------- test-js/test-formatters.js | 107 +++++++++++++++++++++++++++ test-js/test-utils.js | 102 ------------------------- 5 files changed, 186 insertions(+), 173 deletions(-) create mode 100644 src/fontra/client/core/formatters.js create mode 100644 test-js/test-formatters.js diff --git a/src/fontra/client/core/formatters.js b/src/fontra/client/core/formatters.js new file mode 100644 index 000000000..fcde5a3b5 --- /dev/null +++ b/src/fontra/client/core/formatters.js @@ -0,0 +1,72 @@ +// TODO: Keep in mind, that we have NumberFormatter in ui-utils.js. +// _NumberFormatter is different because of: +// NumberFormatter.fromString(0) == undefined -> but should be 0 +// NumberFormatter.fromString(true) == 1 -> but should be undefined +export const _NumberFormatter = { + toString: (value) => value.toString(), + fromString: (value) => { + if (typeof value === "number") { + return { value: value }; + } else if (typeof value === "boolean" || !value) { + return { error: "not a number" }; + } + const number = Number(value); + if (isNaN(number)) { + return { error: "not a number" }; + } else { + return { value: number }; + } + }, +}; + +export const BooleanFormatter = { + toString: (value) => value.toString(), + fromString: (value) => { + if (typeof value === "boolean") { + return { value: value }; + } + if (value.trim().toLowerCase() === "true") { + return { value: true }; + } else if (value.trim().toLowerCase() === "false") { + return { value: false }; + } else { + return { error: "not a boolean" }; + } + }, +}; + +export const ArrayFormatter = { + toString: (value) => { + if (Array.isArray(value)) { + return value.toString(); + } + }, + fromString: (value) => { + const array = JSON.parse("[" + value + "]"); + if (Array.isArray(array)) { + return { value: array }; + } else { + return { error: "not an array" }; + } + }, +}; + +export const NumberArrayFormatter = { + toString: (value) => value.toString(), + fromString: (value, arrayLength) => { + const array = JSON.parse("[" + value + "]"); + if (Array.isArray(array)) { + if (arrayLength && array.length != arrayLength) { + return { error: `array length must be ${arrayLength}` }; + } + return { value: array }; + } else { + return { error: "not an array" }; + } + }, +}; + +export const PanoseArrayFormatter = { + toString: (value) => NumberArrayFormatter.toString(value), + fromString: (value) => NumberArrayFormatter.fromString(value, 10), +}; diff --git a/src/fontra/client/core/ui-utils.js b/src/fontra/client/core/ui-utils.js index 5886334aa..cb93cd842 100644 --- a/src/fontra/client/core/ui-utils.js +++ b/src/fontra/client/core/ui-utils.js @@ -243,7 +243,7 @@ export const DefaultFormatter = { }, }; -// TODO: Maybe move formatters to utils.js, so we can write unittests for it. +// TODO: Move to formatters.js export const NumberFormatter = { toString: (value) => value.toString(), fromString: (value) => { diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index 8bf4f10a1..c1abaa952 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -1,4 +1,10 @@ import { strFromU8, strToU8, unzlibSync, zlibSync } from "../third-party/fflate.js"; +import { + BooleanFormatter, + NumberArrayFormatter, + PanoseArrayFormatter, + _NumberFormatter, +} from "./formatters.js"; import { Transform } from "./transform.js"; export function objectsEqual(obj1, obj2) { @@ -806,76 +812,6 @@ export const friendlyHttpStatus = { 505: "HTTP Version Not Supported", }; -// TODO: This is a temporary solution. Need further refactoring. -export const _NumberFormatter = { - toString: (value) => value.toString(), - fromString: (value) => { - if (typeof value === "number") { - return { value: value }; - } else if (typeof value === "boolean" || !value) { - return { error: "not a number" }; - } - const number = Number(value); - if (isNaN(number)) { - return { error: "not a number" }; - } else { - return { value: number }; - } - }, -}; - -export const BooleanFormatter = { - toString: (value) => value.toString(), - fromString: (value) => { - if (typeof value === "boolean") { - return { value: value }; - } - if (value.trim().toLowerCase() === "true") { - return { value: true }; - } else if (value.trim().toLowerCase() === "false") { - return { value: false }; - } else { - return { error: "not a boolean" }; - } - }, -}; - -export const ArrayFormatter = { - toString: (value) => { - if (Array.isArray(value)) { - return value.toString(); - } - }, - fromString: (value) => { - const array = JSON.parse("[" + value + "]"); - if (Array.isArray(array)) { - return { value: array }; - } else { - return { error: "not an array" }; - } - }, -}; - -export const NumberArrayFormatter = { - toString: (value) => value.toString(), - fromString: (value, arrayLength) => { - const array = JSON.parse("[" + value + "]"); - if (Array.isArray(array)) { - if (arrayLength && array.length != arrayLength) { - return { error: `array length must be ${arrayLength}` }; - } - return { value: array }; - } else { - return { error: "not an array" }; - } - }, -}; - -export const PanoseArrayFormatter = { - toString: (value) => NumberArrayFormatter.toString(value), - fromString: (value) => NumberArrayFormatter.fromString(value, 10), -}; - function getAscenderDefault(fontSource = undefined) { return fontSource.lineMetricsHorizontalLayout.ascender.value || 800; } diff --git a/test-js/test-formatters.js b/test-js/test-formatters.js new file mode 100644 index 000000000..f7c9c4cca --- /dev/null +++ b/test-js/test-formatters.js @@ -0,0 +1,107 @@ +import { expect } from "chai"; +import { + ArrayFormatter, + BooleanFormatter, + NumberArrayFormatter, + _NumberFormatter, +} from "../src/fontra/client/core/formatters.js"; + +import { getTestData, parametrize } from "./test-support.js"; + +describe("NumberFormatter", () => { + parametrize( + "NumberFormatter tests", + [ + ["1", 1], + ["11234", 11234], + ["0", 0], + ["-200", -200], + ["asdfg200", undefined], + ["", undefined], + ["test", undefined], + [undefined, undefined], + [true, undefined], + [false, undefined], + [null, undefined], + [200, 200], + [0, 0], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(_NumberFormatter.fromString(input).value).to.equal(expectedResult); + } + ); +}); + +describe("ArrayFormatter", () => { + parametrize( + "ArrayFormatter fromString tests", + [ + ["1,2,3,4", [1, 2, 3, 4]], + ["1, 2,3,4", [1, 2, 3, 4]], + ["", []], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(ArrayFormatter.fromString(input).value).to.deep.equal(expectedResult); + } + ); +}); + +describe("ArrayFormatter", () => { + parametrize( + "ArrayFormatter toString tests", + [ + [[1, 2, 3, 4], "1,2,3,4"], + [[], ""], + [true, undefined], + [new Set([1, 2, 3]), undefined], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(ArrayFormatter.toString(input)).to.deep.equal(expectedResult); + } + ); +}); + +describe("NumberArrayFormatter", () => { + parametrize( + "NumberArrayFormatter fromString tests", + [ + ["1,2,3,4", [1, 2, 3, 4], 4], + ["1, 2,3, 4", [1, 2, 3, 4], 4], + ["1, 2,3,4", undefined, 3], + ["", [], 0], + ], + (testData) => { + const [input, expectedResult, arrayLength] = testData; + expect(NumberArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( + expectedResult + ); + } + ); +}); + +describe("BooleanFormatter", () => { + parametrize( + "BooleanFormatter fromString tests", + [ + ["false", false], + ["true", true], + ["False", false], + ["True", true], + ["FALSE", false], + ["TRUE", true], + [false, false], + [true, true], + ["", undefined], + ["Hello", undefined], + [" false ", false], + [" true ", true], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(BooleanFormatter.fromString(input).value).to.deep.equal(expectedResult); + } + ); +}); diff --git a/test-js/test-utils.js b/test-js/test-utils.js index f30bcc0ab..c3ea8ea21 100644 --- a/test-js/test-utils.js +++ b/test-js/test-utils.js @@ -1,9 +1,5 @@ import { expect } from "chai"; import { - ArrayFormatter, - BooleanFormatter, - NumberArrayFormatter, - _NumberFormatter, arrayExtend, bisect_right, boolInt, @@ -752,101 +748,3 @@ describe("bisect_right", () => { expect(bisect_right(testCase.a, testCase.x)).to.equal(testCase.i); }); }); - -describe("NumberFormatter", () => { - parametrize( - "NumberFormatter tests", - [ - ["1", 1], - ["11234", 11234], - ["0", 0], - ["-200", -200], - ["asdfg200", undefined], - ["", undefined], - ["test", undefined], - [undefined, undefined], - [true, undefined], - [false, undefined], - [null, undefined], - [200, 200], - [0, 0], - ], - (testData) => { - const [input, expectedResult] = testData; - expect(_NumberFormatter.fromString(input).value).to.equal(expectedResult); - } - ); -}); - -describe("ArrayFormatter", () => { - parametrize( - "ArrayFormatter fromString tests", - [ - ["1,2,3,4", [1, 2, 3, 4]], - ["1, 2,3,4", [1, 2, 3, 4]], - ["", []], - ], - (testData) => { - const [input, expectedResult] = testData; - expect(ArrayFormatter.fromString(input).value).to.deep.equal(expectedResult); - } - ); -}); - -describe("ArrayFormatter", () => { - parametrize( - "ArrayFormatter toString tests", - [ - [[1, 2, 3, 4], "1,2,3,4"], - [[], ""], - [true, undefined], - [new Set([1, 2, 3]), undefined], - ], - (testData) => { - const [input, expectedResult] = testData; - expect(ArrayFormatter.toString(input)).to.deep.equal(expectedResult); - } - ); -}); - -describe("NumberArrayFormatter", () => { - parametrize( - "NumberArrayFormatter fromString tests", - [ - ["1,2,3,4", [1, 2, 3, 4], 4], - ["1, 2,3, 4", [1, 2, 3, 4], 4], - ["1, 2,3,4", undefined, 3], - ["", [], 0], - ], - (testData) => { - const [input, expectedResult, arrayLength] = testData; - expect(NumberArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( - expectedResult - ); - } - ); -}); - -describe("BooleanFormatter", () => { - parametrize( - "BooleanFormatter fromString tests", - [ - ["false", false], - ["true", true], - ["False", false], - ["True", true], - ["FALSE", false], - ["TRUE", true], - [false, false], - [true, true], - ["", undefined], - ["Hello", undefined], - [" false ", false], - [" true ", true], - ], - (testData) => { - const [input, expectedResult] = testData; - expect(BooleanFormatter.fromString(input).value).to.deep.equal(expectedResult); - } - ); -}); From 3e754442054ee5e1fbd176f8c6d75ca6dc6ee826 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 10:46:27 +0100 Subject: [PATCH 18/33] Revert testdata customData keys + ufoInfoAttributesToRoundTrip --- src/fontra/backends/designspace.py | 172 ++++++++---------- .../fonts/MutatorSans.fontra/font-data.json | 4 +- .../font-data.json | 4 +- .../font-data.json | 4 +- .../output-subset-scale.fontra/font-data.json | 4 +- test-py/test_backends_designspace.py | 4 +- 6 files changed, 90 insertions(+), 102 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 838d8491f..45a0691af 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -62,9 +62,6 @@ from .filewatcher import Change, FileWatcher from .ufo_utils import extractGlyphNameAndCodePoints -# from glyphsLib.builder.custom_params import KNOWN_PARAM_HANDLERS - - logger = logging.getLogger(__name__) @@ -126,84 +123,78 @@ ("vendorID", "openTypeOS2VendorID"), ] -# TODO: Alternative idea: Use glyphsLib KNOWN_PARAM_HANDLERS to create the mapping -# customDataNameMapping = {} -# for handler in KNOWN_PARAM_HANDLERS: -# try: -# key = handler.glyphs_name -# except AttributeError: -# # Handler without glyphs_name -# continue -# # print(f"{key}: {handler.ufo_name}") -# customDataNameMapping[key] = handler.ufo_name +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", +] customDataNameMapping = { # Fontra / UFO - "gaspRangeRecords": "openTypeGaspRangeRecords", - "created": "openTypeHeadCreated", - "headFlags": "openTypeHeadFlags", - "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", - "hheaAscender": "openTypeHheaAscender", - "hheaCaretOffset": "openTypeHheaCaretOffset", - "hheaCaretSlopeRise": "openTypeHheaCaretSlopeRise", - "hheaCaretSlopeRun": "openTypeHheaCaretSlopeRun", - "hheaDescender": "openTypeHheaDescender", - "hheaLineGap": "openTypeHheaLineGap", - "compatibleFullName": "openTypeNameCompatibleFullName", - "preferredFamilyName": "openTypeNamePreferredFamilyName", - "preferredSubfamilyName": "openTypeNamePreferredSubfamilyName", - "records": "openTypeNameRecords", - "uniqueID": "openTypeNameUniqueID", - "versionString": "openTypeNameVersion", - "WWSFamilyName": "openTypeNameWWSFamilyName", - "WWSSubfamilyName": "openTypeNameWWSSubfamilyName", - "codePageRanges": "openTypeOS2CodePageRanges", - "familyClass": "openTypeOS2FamilyClass", - "panose": "openTypeOS2Panose", - "fsSelection": "openTypeOS2Selection", - "strikeoutPosition": "openTypeOS2StrikeoutPosition", - "strikeoutSize": "openTypeOS2StrikeoutSize", - "subscriptXOffset": "openTypeOS2SubscriptXOffset", - "subscriptXSize": "openTypeOS2SubscriptXSize", - "subscriptYOffset": "openTypeOS2SubscriptYOffset", - "subscriptYSize": "openTypeOS2SubscriptYSize", - "superscriptXOffset": "openTypeOS2SuperscriptXOffset", - "superscriptXSize": "openTypeOS2SuperscriptXSize", - "superscriptYOffset": "openTypeOS2SuperscriptYOffset", - "superscriptYSize": "openTypeOS2SuperscriptYSize", - "fsType": "openTypeOS2Type", - "typoAscender": "openTypeOS2TypoAscender", - "typoDescender": "openTypeOS2TypoDescender", - "typoLineGap": "openTypeOS2TypoLineGap", - "unicodeRanges": "openTypeOS2UnicodeRanges", - "weightClass": "openTypeOS2WeightClass", - "widthClass": "openTypeOS2WidthClass", - "winAscent": "openTypeOS2WinAscent", - "winDescent": "openTypeOS2WinDescent", - "vheaCaretOffset": "openTypeVheaCaretOffset", - "vheaCaretSlopeRise": "openTypeVheaCaretSlopeRise", - "vheaCaretSlopeRun": "openTypeVheaCaretSlopeRun", - "vheaVertTypoLineGap": "openTypeVheaVertTypoLineGap", - "blueFuzz": "postscriptBlueFuzz", - "blueScale": "postscriptBlueScale", - "blueShift": "postscriptBlueShift", - "blueValues": "postscriptBlueValues", - "defaultCharacter": "postscriptDefaultCharacter", - "defaultWidthX": "postscriptDefaultWidthX", - "familyBlues": "postscriptFamilyBlues", - "familyOtherBlues": "postscriptFamilyOtherBlues", - "forceBold": "postscriptForceBold", - "isFixedPitch": "postscriptIsFixedPitch", - "nominalWidthX": "postscriptNominalWidthX", - "otherBlues": "postscriptOtherBlues", - "slantAngle": "postscriptSlantAngle", - "stemSnapH": "postscriptStemSnapH", - "stemSnapV": "postscriptStemSnapV", - "underlinePosition": "postscriptUnderlinePosition", - "underlineThickness": "postscriptUnderlineThickness", - "psUniqueID": "postscriptUniqueID", # inconsistent psXXX, because uniqueID exists already. - "weightName": "postscriptWeightName", - "windowsCharacterSet": "postscriptWindowsCharacterSet", } @@ -1630,10 +1621,10 @@ def asFontraFontSource(self, unitsPerEm: int) -> FontSource: guidelines = unpackGuidelines(fontInfo.guidelines) italicAngle = getattr(fontInfo, "italicAngle", 0) - for infoAttrFontra, infoAttrUFO in customDataNameMapping.items(): - value = getattr(fontInfo, infoAttrUFO, None) + for infoAttr in ufoInfoAttributesToRoundTrip: + value = getattr(fontInfo, infoAttr, None) if value is not None: - customData[infoAttrFontra] = value + customData[f"{ufoInfoPrefix}{infoAttr}"] = value return FontSource( name=self.name, @@ -2186,20 +2177,17 @@ def updateFontInfoFromFontSource(reader, fontSource): # set custom data for key, value in fontSource.customData.items(): - ufoName = customDataNameMapping.get(key) - if ufoName is not None: - setattr(fontInfo, ufoName, value) - else: - raise NotImplementedError( - f"The CustomData attribute '{key}' lacks a UFO equivalent" - ) + if key.startswith(ufoInfoPrefix): + infoAttr = key[len(ufoInfoPrefix) :] + setattr(fontInfo, infoAttr, value) # delete custom data - for fontraName, ufoName in customDataNameMapping.items(): - if fontraName not in fontSource.customData.keys(): - value = getattr(fontInfo, ufoName, None) + for infoAttr in ufoInfoAttributesToRoundTrip: + customDataKay = f"{ufoInfoPrefix}{infoAttr}" + if customDataKay not in fontSource.customData.keys(): + value = getattr(fontInfo, infoAttr, None) if value is not None: - delattr(fontInfo, ufoName) + delattr(fontInfo, infoAttr) reader.writeInfo(fontInfo) diff --git a/test-common/fonts/MutatorSans.fontra/font-data.json b/test-common/fonts/MutatorSans.fontra/font-data.json index 52764f7a8..487d71813 100644 --- a/test-common/fonts/MutatorSans.fontra/font-data.json +++ b/test-common/fonts/MutatorSans.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"typoAscender": 700, -"typoDescender": -200 +"ufo.info.openTypeOS2TypoAscender": 700, +"ufo.info.openTypeOS2TypoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json b/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json index 52764f7a8..487d71813 100644 --- a/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-glyphs-kerning.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"typoAscender": 700, -"typoDescender": -200 +"ufo.info.openTypeOS2TypoAscender": 700, +"ufo.info.openTypeOS2TypoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json b/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json index 52764f7a8..487d71813 100644 --- a/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-scale-kerning.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"typoAscender": 700, -"typoDescender": -200 +"ufo.info.openTypeOS2TypoAscender": 700, +"ufo.info.openTypeOS2TypoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/data/workflow/output-subset-scale.fontra/font-data.json b/test-py/data/workflow/output-subset-scale.fontra/font-data.json index 52764f7a8..487d71813 100644 --- a/test-py/data/workflow/output-subset-scale.fontra/font-data.json +++ b/test-py/data/workflow/output-subset-scale.fontra/font-data.json @@ -165,8 +165,8 @@ } ], "customData": { -"typoAscender": 700, -"typoDescender": -200 +"ufo.info.openTypeOS2TypoAscender": 700, +"ufo.info.openTypeOS2TypoDescender": -200 } }, "light-condensed-italic": { diff --git a/test-py/test_backends_designspace.py b/test-py/test_backends_designspace.py index 140258f20..ac3c5fcef 100644 --- a/test-py/test_backends_designspace.py +++ b/test-py/test_backends_designspace.py @@ -631,8 +631,8 @@ async def test_findGlyphsThatUseGlyph(writableTestFont): {"name": "Guideline Baseline Overshoot", "y": -10}, ], "customData": { - "typoAscender": 700, - "typoDescender": -200, + "ufo.info.openTypeOS2TypoAscender": 700, + "ufo.info.openTypeOS2TypoDescender": -200, }, }, { From bca771565d9c3bb06993588495ecff491338ab6c Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 10:48:17 +0100 Subject: [PATCH 19/33] remove leftover 'customDataNameMapping' --- src/fontra/backends/designspace.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index 45a0691af..f6e9e621f 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -123,8 +123,10 @@ ("vendorID", "openTypeOS2VendorID"), ] + ufoInfoPrefix = "ufo.info." + ufoInfoAttributesToRoundTrip = [ "openTypeGaspRangeRecords", "openTypeHeadCreated", @@ -193,10 +195,6 @@ "postscriptWindowsCharacterSet", ] -customDataNameMapping = { - # Fontra / UFO -} - class DesignspaceBackend: @classmethod From 1d6d1908f41632039a725e3e4ef4b96d6aa6c310 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 11:43:03 +0100 Subject: [PATCH 20/33] Move customDataNameMapping from utils.js to customData.js --- src/fontra/client/core/customData.js | 131 +++++++++++++++ src/fontra/client/core/utils.js | 243 --------------------------- 2 files changed, 131 insertions(+), 243 deletions(-) create mode 100644 src/fontra/client/core/customData.js diff --git a/src/fontra/client/core/customData.js b/src/fontra/client/core/customData.js new file mode 100644 index 000000000..6dc2c67fe --- /dev/null +++ b/src/fontra/client/core/customData.js @@ -0,0 +1,131 @@ +import { + BooleanFormatter, + NumberArrayFormatter, + PanoseArrayFormatter, + _NumberFormatter, +} from "./formatters.js"; + +function getAscenderDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.ascender.value || 800; +} + +function getDescenderDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.descender.value || -200; +} + +function getDescenderWinDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.descender.value * -1 || 200; +} + +function getFamilyNameDefault(fontSource = undefined) { + return fontSource.familyName || "Family Name"; +} + +function getSubfamilyNameDefault(fontSource = undefined) { + return fontSource.name || "Subfamily Name"; +} + +function getstrikeoutPositionDefault(fontSource = undefined) { + return fontSource.lineMetricsHorizontalLayout.ascender.value / 2 || 250; +} + +function getCreatedDefault() { + // Note: UTC might differ from your local time. + const date = new Date(); + + const YYYY = date.getUTCFullYear(); + const MM = String(date.getUTCMonth() + 1).padStart(2, "0"); + const DD = String(date.getUTCDate()).padStart(2, "0"); + + const HH = String(date.getUTCHours()).padStart(2, "0"); + const mm = String(date.getUTCMinutes()).padStart(2, "0"); + const SS = String(date.getUTCSeconds()).padStart(2, "0"); + + return `${YYYY}/${MM}/${DD} ${HH}:${mm}:${SS}`; // "YYYY/MM/DD HH:MM:SS" +} + +export const customDataNameMapping = { + // verticl metrics values + openTypeHheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, + openTypeHheaDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, + openTypeHheaLineGap: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2TypoAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, + openTypeOS2TypoDescender: { + default: getDescenderDefault, + formatter: _NumberFormatter, + }, + openTypeOS2TypoLineGap: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2WinAscent: { default: getAscenderDefault, formatter: _NumberFormatter }, + openTypeOS2WinDescent: { + default: getDescenderWinDefault, + formatter: _NumberFormatter, + }, + postscriptUnderlinePosition: { default: () => -100, formatter: _NumberFormatter }, + postscriptUnderlineThickness: { default: () => 50, formatter: _NumberFormatter }, + openTypeOS2StrikeoutPosition: { + default: getstrikeoutPositionDefault, + formatter: _NumberFormatter, + }, + openTypeOS2StrikeoutSize: { default: () => 50, formatter: _NumberFormatter }, + // name table entries + openTypeNameUniqueID: { default: () => "uniqueID Name ID 3" }, // Name ID 3 + openTypeNameVersion: { default: () => "Version 1.0" }, // Name ID 7 + openTypeNamePreferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 + openTypeNamePreferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 + openTypeNameCompatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 + openTypeNameWWSFamilyName: { default: getFamilyNameDefault }, // Name ID 21 + openTypeNameWWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22 + // misc + openTypeOS2WeightClass: { default: () => 400, formatter: _NumberFormatter }, + openTypeOS2WidthClass: { default: () => 5, formatter: _NumberFormatter }, + openTypeHeadCreated: { default: getCreatedDefault }, // The timezone is UTC. + openTypeOS2Selection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 + openTypeOS2Type: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 + openTypeOS2Panose: { + default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], + formatter: PanoseArrayFormatter, + }, // default: sans-serif + openTypeOS2UnicodeRanges: { default: () => [], formatter: NumberArrayFormatter }, + openTypeOS2CodePageRanges: { default: () => [], formatter: NumberArrayFormatter }, + // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf + postscriptBlueValues: { default: () => [], formatter: NumberArrayFormatter }, + postscriptOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, + postscriptFamilyBlues: { default: () => [], formatter: NumberArrayFormatter }, + postscriptFamilyOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, + postscriptBlueScale: { default: () => 0.039625, formatter: _NumberFormatter }, + postscriptBlueShift: { default: () => 1, formatter: _NumberFormatter }, + postscriptBlueFuzz: { default: () => 1, formatter: _NumberFormatter }, + postscriptStemSnapH: { default: () => [], formatter: NumberArrayFormatter }, + postscriptStemSnapV: { default: () => [], formatter: NumberArrayFormatter }, + postscriptForceBold: { default: () => false, formatter: BooleanFormatter }, +}; + +// TODO: Based on ufoInfoAttributesToRoundTrip (designspace.py) +// "openTypeGaspRangeRecords", +// "openTypeHeadFlags", +// "openTypeHeadLowestRecPPEM", +// "openTypeHheaCaretOffset", +// "openTypeHheaCaretSlopeRise", +// "openTypeHheaCaretSlopeRun", +// "openTypeNameRecords", +// "openTypeOS2FamilyClass", +// "openTypeOS2SubscriptXOffset", +// "openTypeOS2SubscriptXSize", +// "openTypeOS2SubscriptYOffset", +// "openTypeOS2SubscriptYSize", +// "openTypeOS2SuperscriptXOffset", +// "openTypeOS2SuperscriptXSize", +// "openTypeOS2SuperscriptYOffset", +// "openTypeOS2SuperscriptYSize", +// "openTypeVheaCaretOffset", +// "openTypeVheaCaretSlopeRise", +// "openTypeVheaCaretSlopeRun", +// "openTypeVheaVertTypoLineGap", +// "postscriptDefaultCharacter", +// "postscriptDefaultWidthX", +// "postscriptIsFixedPitch", +// "postscriptNominalWidthX", +// "postscriptSlantAngle", +// "postscriptUniqueID", +// "postscriptWeightName", +// "postscriptWindowsCharacterSet", diff --git a/src/fontra/client/core/utils.js b/src/fontra/client/core/utils.js index c1abaa952..c8aede7a2 100644 --- a/src/fontra/client/core/utils.js +++ b/src/fontra/client/core/utils.js @@ -1,10 +1,4 @@ import { strFromU8, strToU8, unzlibSync, zlibSync } from "../third-party/fflate.js"; -import { - BooleanFormatter, - NumberArrayFormatter, - PanoseArrayFormatter, - _NumberFormatter, -} from "./formatters.js"; import { Transform } from "./transform.js"; export function objectsEqual(obj1, obj2) { @@ -811,240 +805,3 @@ export const friendlyHttpStatus = { 504: "Gateway Timeout", 505: "HTTP Version Not Supported", }; - -function getAscenderDefault(fontSource = undefined) { - return fontSource.lineMetricsHorizontalLayout.ascender.value || 800; -} - -function getDescenderDefault(fontSource = undefined) { - return fontSource.lineMetricsHorizontalLayout.descender.value || -200; -} - -function getDescenderWinDefault(fontSource = undefined) { - return fontSource.lineMetricsHorizontalLayout.descender.value * -1 || 200; -} - -function getFamilyNameDefault(fontSource = undefined) { - return fontSource.familyName || "Family Name"; -} - -function getSubfamilyNameDefault(fontSource = undefined) { - return fontSource.name || "Subfamily Name"; -} - -function getstrikeoutPositionDefault(fontSource = undefined) { - return fontSource.lineMetricsHorizontalLayout.ascender.value / 2 || 250; -} - -function getCreatedDefault() { - // Note: UTC might differ from your local time. - const date = new Date(); - - const YYYY = date.getUTCFullYear(); - const MM = String(date.getUTCMonth() + 1).padStart(2, "0"); - const DD = String(date.getUTCDate()).padStart(2, "0"); - - const HH = String(date.getUTCHours()).padStart(2, "0"); - const mm = String(date.getUTCMinutes()).padStart(2, "0"); - const SS = String(date.getUTCSeconds()).padStart(2, "0"); - - return `${YYYY}/${MM}/${DD} ${HH}:${mm}:${SS}`; // "YYYY/MM/DD HH:MM:SS" -} - -export const customDataNameMapping = { - // verticl metrics values - hheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, - hheaDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, - hheaLineGap: { default: () => 0, formatter: _NumberFormatter }, - typoAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, - typoDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, - typoLineGap: { default: () => 0, formatter: _NumberFormatter }, - winAscent: { default: getAscenderDefault, formatter: _NumberFormatter }, - winDescent: { default: getDescenderWinDefault, formatter: _NumberFormatter }, - underlinePosition: { default: () => -100, formatter: _NumberFormatter }, - underlineThickness: { default: () => 50, formatter: _NumberFormatter }, - strikeoutPosition: { - default: getstrikeoutPositionDefault, - formatter: _NumberFormatter, - }, - strikeoutSize: { default: () => 50, formatter: _NumberFormatter }, - // name table entries - uniqueID: { default: () => "uniqueID Name ID 3" }, // Name ID 3 - versionString: { default: () => "Version 1.0" }, // Name ID 7 - preferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 - preferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 - compatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 - WWSFamilyName: { default: getFamilyNameDefault }, // Name ID 21 - WWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22 - // misc - weightClass: { default: () => 400, formatter: _NumberFormatter }, - widthClass: { default: () => 5, formatter: _NumberFormatter }, - created: { default: getCreatedDefault }, // The timezone is UTC. - fsSelection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 - fsType: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 - panose: { - default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], - formatter: PanoseArrayFormatter, - }, // default: sans-serif - unicodeRanges: { default: () => [], formatter: NumberArrayFormatter }, - codePageRanges: { default: () => [], formatter: NumberArrayFormatter }, - // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf - blueValues: { default: () => [], formatter: NumberArrayFormatter }, - otherBlues: { default: () => [], formatter: NumberArrayFormatter }, - familyBlues: { default: () => [], formatter: NumberArrayFormatter }, - familyOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, - blueScale: { default: () => 0.039625, formatter: _NumberFormatter }, - blueShift: { default: () => 1, formatter: _NumberFormatter }, - blueFuzz: { default: () => 1, formatter: _NumberFormatter }, - stemSnapH: { default: () => [], formatter: NumberArrayFormatter }, - stemSnapV: { default: () => [], formatter: NumberArrayFormatter }, - forceBold: { default: () => false, formatter: BooleanFormatter }, -}; - -// TODO: Based on customDataNameMapping (designspace.py) -// "gaspRangeRecords": "openTypeGaspRangeRecords", -// "headFlags": "openTypeHeadFlags", -// "headLowestRecPPEM": "openTypeHeadLowestRecPPEM", -// "hheaCaretOffset": "openTypeHheaCaretOffset", -// "hheaCaretSlopeRise": "openTypeHheaCaretSlopeRise", -// "hheaCaretSlopeRun": "openTypeHheaCaretSlopeRun", -// "records": "openTypeNameRecords", -// "uniqueID": "openTypeNameUniqueID", -// "codePageRanges": "openTypeOS2CodePageRanges", -// "familyClass": "openTypeOS2FamilyClass", -// "subscriptXOffset": "openTypeOS2SubscriptXOffset", -// "subscriptXSize": "openTypeOS2SubscriptXSize", -// "subscriptYOffset": "openTypeOS2SubscriptYOffset", -// "subscriptYSize": "openTypeOS2SubscriptYSize", -// "superscriptXOffset": "openTypeOS2SuperscriptXOffset", -// "superscriptXSize": "openTypeOS2SuperscriptXSize", -// "superscriptYOffset": "openTypeOS2SuperscriptYOffset", -// "superscriptYSize": "openTypeOS2SuperscriptYSize", -// "unicodeRanges": "openTypeOS2UnicodeRanges", -// "vheaCaretOffset": "openTypeVheaCaretOffset", -// "vheaCaretSlopeRise": "openTypeVheaCaretSlopeRise", -// "vheaCaretSlopeRun": "openTypeVheaCaretSlopeRun", -// "vheaVertTypoLineGap": "openTypeVheaVertTypoLineGap", -// "blueFuzz": "postscriptBlueFuzz", -// "blueScale": "postscriptBlueScale", -// "blueShift": "postscriptBlueShift", -// "blueValues": "postscriptBlueValues", -// "defaultCharacter": "postscriptDefaultCharacter", -// "defaultWidthX": "postscriptDefaultWidthX", -// "familyBlues": "postscriptFamilyBlues", -// "familyOtherBlues": "postscriptFamilyOtherBlues", -// "forceBold": "postscriptForceBold", -// "isFixedPitch": "postscriptIsFixedPitch", -// "nominalWidthX": "postscriptNominalWidthX", -// "otherBlues": "postscriptOtherBlues", -// "slantAngle": "postscriptSlantAngle", -// "stemSnapH": "postscriptStemSnapH", -// "stemSnapV": "postscriptStemSnapV", -// "psUniqueID": "postscriptUniqueID", # inconsistent psXXX, because uniqueID exists already. -// "weightName": "postscriptWeightName", -// "windowsCharacterSet": "postscriptWindowsCharacterSet", - -// GlyphsApp to UFO mapping -// compatibleFullName: openTypeNameCompatibleFullName -// hheaAscender: openTypeHheaAscender -// hheaDescender: openTypeHheaDescender -// hheaLineGap: openTypeHheaLineGap -// panose: openTypeOS2Panose -// fsType: openTypeOS2Type -// typoAscender: openTypeOS2TypoAscender -// typoDescender: openTypeOS2TypoDescender -// typoLineGap: openTypeOS2TypoLineGap -// unicodeRanges: openTypeOS2UnicodeRanges -// strikeoutSize: openTypeOS2StrikeoutSize -// strikeoutPosition: openTypeOS2StrikeoutPosition -// subscriptXSize: openTypeOS2SubscriptXSize -// subscriptYSize: openTypeOS2SubscriptYSize -// subscriptXOffset: openTypeOS2SubscriptXOffset -// subscriptYOffset: openTypeOS2SubscriptYOffset -// superscriptXSize: openTypeOS2SuperscriptXSize -// superscriptYSize: openTypeOS2SuperscriptYSize -// superscriptXOffset: openTypeOS2SuperscriptXOffset -// superscriptYOffset: openTypeOS2SuperscriptYOffset -// vheaVertAscender: openTypeVheaVertTypoAscender -// vheaVertDescender: openTypeVheaVertTypoDescender -// vheaVertLineGap: openTypeVheaVertTypoLineGap -// vheaVertTypoAscender: openTypeVheaVertTypoAscender -// vheaVertTypoDescender: openTypeVheaVertTypoDescender -// vheaVertTypoLineGap: openTypeVheaVertTypoLineGap -// blueScale: postscriptBlueScale -// blueShift: postscriptBlueShift -// isFixedPitch: postscriptIsFixedPitch -// underlinePosition: postscriptUnderlinePosition -// underlineThickness: postscriptUnderlineThickness -// versionString: openTypeNameVersion -// vendorID: openTypeOS2VendorID -// uniqueID: openTypeNameUniqueID -// license: openTypeNameLicense -// licenseURL: openTypeNameLicenseURL -// trademark: trademark -// description: openTypeNameDescription -// sampleText: openTypeNameSampleText -// postscriptFullName: postscriptFullName -// postscriptFontName: postscriptFontName -// preferredFamilyName: openTypeNamePreferredFamilyName -// preferredSubfamilyName: openTypeNamePreferredSubfamilyName -// WWSFamilyName: openTypeNameWWSFamilyName -// WWSSubfamilyName: openTypeNameWWSSubfamilyName -// openTypeHheaCaretSlopeRun: openTypeHheaCaretSlopeRun -// openTypeVheaCaretSlopeRun: openTypeVheaCaretSlopeRun -// openTypeHheaCaretSlopeRise: openTypeHheaCaretSlopeRise -// openTypeVheaCaretSlopeRise: openTypeVheaCaretSlopeRise -// openTypeHheaCaretOffset: openTypeHheaCaretOffset -// openTypeVheaCaretOffset: openTypeVheaCaretOffset -// openTypeHeadLowestRecPPEM: openTypeHeadLowestRecPPEM -// openTypeHeadFlags: openTypeHeadFlags -// openTypeNameVersion: openTypeNameVersion -// openTypeNameUniqueID: openTypeNameUniqueID -// openTypeOS2FamilyClass: openTypeOS2FamilyClass -// postscriptSlantAngle: postscriptSlantAngle -// postscriptUniqueID: postscriptUniqueID -// postscriptBlueFuzz: postscriptBlueFuzz -// postscriptForceBold: postscriptForceBold -// postscriptDefaultWidthX: postscriptDefaultWidthX -// postscriptNominalWidthX: postscriptNominalWidthX -// postscriptWeightName: postscriptWeightName -// postscriptDefaultCharacter: postscriptDefaultCharacter -// postscriptWindowsCharacterSet: postscriptWindowsCharacterSet -// macintoshFONDFamilyID: macintoshFONDFamilyID -// macintoshFONDName: macintoshFONDName -// styleMapFamilyName: styleMapFamilyName -// styleMapStyleName: styleMapStyleName -// postscriptFamilyBlues: postscriptFamilyBlues -// postscriptFamilyOtherBlues: postscriptFamilyOtherBlues -// winAscent: openTypeOS2WinAscent -// winDescent: openTypeOS2WinDescent -// weightClass: openTypeOS2WeightClass -// widthClass: openTypeOS2WidthClass -// GASP Table: openTypeGaspRangeRecords -// gasp Table: openTypeGaspRangeRecords -// meta Table: public.openTypeMeta -// Color Palettes: com.github.googlei18n.ufo2ft.colorPalettes -// Disable Last Change: disablesLastChange -// Don't use Production Names: com.github.googlei18n.ufo2ft.useProductionNames -// disablesAutomaticAlignment: disablesAutomaticAlignment -// iconName: iconName -// DisplayStrings: DisplayStrings -// disablesNiceNames: useNiceNames -// customValue: customValue -// customValue1: customValue1 -// customValue2: customValue2 -// customValue3: customValue3 -// weightValue: weightValue -// widthValue: widthValue -// Virtual Master: Virtual Master - -// Handler without glyphs_name -// -// -// -// -// -// -// -// -// From 33448389c6599f48f173c88b225a2d17c748512e Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 11:44:10 +0100 Subject: [PATCH 21/33] Remove ufoInfoPrefix in UI + add "not implemented" if key not in customDataNameMapping --- src/fontra/views/fontinfo/panel-sources.js | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 5a7ae0510..8fe33b221 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -1,5 +1,6 @@ import { doPerformAction, getActionIdentifierFromKeyEvent } from "../core/actions.js"; import { recordChanges } from "../core/change-recorder.js"; +import { customDataNameMapping } from "../core/customData.js"; import * as html from "../core/html-utils.js"; import { addStyleSheet } from "../core/html-utils.js"; import { translate } from "../core/localization.js"; @@ -14,14 +15,7 @@ import { labeledTextInput, textInput, } from "../core/ui-utils.js"; -import { - arrowKeyDeltas, - customDataNameMapping, - enumerate, - modulo, - range, - round, -} from "../core/utils.js"; +import { arrowKeyDeltas, enumerate, modulo, range, round } from "../core/utils.js"; import { UIList } from "../web-components/ui-list.js"; import { arraysEqual, updateRemoveButton } from "./panel-axes.js"; import { BaseInfoPanel } from "./panel-base.js"; @@ -34,6 +28,7 @@ import "/web-components/add-remove-buttons.js"; import "/web-components/designspace-location.js"; import { dialogSetup, message } from "/web-components/modal-dialog.js"; +const ufoInfoPrefix = "ufo.info."; let selectedSourceIdentifier = undefined; addStyleSheet(` @@ -692,10 +687,17 @@ class SourceBox extends HTMLElement { // Skip this, so people can edit this placeholder it. continue; } + if (!customDataNameMapping[key]) { + message( + translate("sources.dialog.cannot-edit-source.title"), + `CustomData "${key}" not implemented, yet.` + ); + continue; + } const formatter = customDataNameMapping[key]?.formatter || DefaultFormatter; const value = formatter.fromString(item["value"]).value; if (value !== undefined) { - source.customData[key] = value; + source.customData[`${ufoInfoPrefix}${key}`] = value; } else { message( translate("sources.dialog.cannot-edit-source.title"), @@ -926,7 +928,10 @@ function buildFontCustomDataList(controller, fontSource) { const model = controller.model; const makeItem = ([key, value]) => { - const item = new ObservableController({ key: key, value: value }); + const keyDisplayed = key.startsWith(ufoInfoPrefix) + ? key.substring(ufoInfoPrefix.length) + : key; + const item = new ObservableController({ key: keyDisplayed, value: value }); item.addListener((event) => { const sortedItems = [...labelList.items]; sortedItems.sort( From 638888cf3665c5370ba36bc94d9986ca90157a73 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 12:30:51 +0100 Subject: [PATCH 22/33] Update customDataNameMapping --- src/fontra/client/core/customData.js | 65 ++++++++++++++++------------ 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/fontra/client/core/customData.js b/src/fontra/client/core/customData.js index 6dc2c67fe..abc78e91f 100644 --- a/src/fontra/client/core/customData.js +++ b/src/fontra/client/core/customData.js @@ -85,6 +85,7 @@ export const customDataNameMapping = { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], formatter: PanoseArrayFormatter, }, // default: sans-serif + openTypeOS2FamilyClass: { default: () => [8, 0], formatter: NumberArrayFormatter }, // Class ID 8 = Sans Serif, Subclass ID = 0: No Classification openTypeOS2UnicodeRanges: { default: () => [], formatter: NumberArrayFormatter }, openTypeOS2CodePageRanges: { default: () => [], formatter: NumberArrayFormatter }, // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf @@ -98,34 +99,42 @@ export const customDataNameMapping = { postscriptStemSnapH: { default: () => [], formatter: NumberArrayFormatter }, postscriptStemSnapV: { default: () => [], formatter: NumberArrayFormatter }, postscriptForceBold: { default: () => false, formatter: BooleanFormatter }, + // PostScript Specific Data + // postscriptFontName // NOTE: not in ufoInfoAttributesToRoundTrip + // postscriptFullName // NOTE: not in ufoInfoAttributesToRoundTrip + postscriptSlantAngle: { default: () => 0.0, formatter: _NumberFormatter }, + postscriptUniqueID: { default: () => 0, formatter: _NumberFormatter }, + postscriptWeightName: { default: () => "postscriptWeightName" }, + postscriptIsFixedPitch: { default: () => false, formatter: BooleanFormatter }, // Indicates if the font is monospaced. + postscriptDefaultWidthX: { default: () => 0, formatter: _NumberFormatter }, + postscriptNominalWidthX: { default: () => 0, formatter: _NumberFormatter }, + postscriptDefaultCharacter: { default: () => "glyphName" }, // The name of the glyph that should be used as the default character in PFM files. + postscriptWindowsCharacterSet: { default: () => 0, formatter: _NumberFormatter }, + // OpenType vhea Table Fields + // openTypeVheaVertTypoAscender // NOTE: not in ufoInfoAttributesToRoundTrip + // openTypeVheaVertTypoDescender // NOTE: not in ufoInfoAttributesToRoundTrip + openTypeVheaVertTypoLineGap: { default: () => 0, formatter: _NumberFormatter }, + openTypeVheaCaretSlopeRise: { default: () => 0, formatter: _NumberFormatter }, + openTypeVheaCaretSlopeRun: { default: () => 0, formatter: _NumberFormatter }, + openTypeVheaCaretOffset: { default: () => 0, formatter: _NumberFormatter }, + // OpenType hhea Table Fields + openTypeHheaCaretSlopeRise: { default: () => 0, formatter: _NumberFormatter }, + openTypeHheaCaretSlopeRun: { default: () => 0, formatter: _NumberFormatter }, + openTypeHheaCaretOffset: { default: () => 0, formatter: _NumberFormatter }, + // OpenType OS/2 Table Fields + openTypeOS2SubscriptXSize: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SubscriptYSize: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SubscriptXOffset: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SubscriptYOffset: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SuperscriptXSize: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SuperscriptYSize: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SuperscriptXOffset: { default: () => 0, formatter: _NumberFormatter }, + openTypeOS2SuperscriptYOffset: { default: () => 0, formatter: _NumberFormatter }, + // OpenType OS/2 Table Fields + openTypeHeadLowestRecPPEM: { default: () => 6, formatter: _NumberFormatter }, // Smallest readable size in pixels. + openTypeHeadFlags: { default: () => [], formatter: NumberArrayFormatter }, }; // TODO: Based on ufoInfoAttributesToRoundTrip (designspace.py) -// "openTypeGaspRangeRecords", -// "openTypeHeadFlags", -// "openTypeHeadLowestRecPPEM", -// "openTypeHheaCaretOffset", -// "openTypeHheaCaretSlopeRise", -// "openTypeHheaCaretSlopeRun", -// "openTypeNameRecords", -// "openTypeOS2FamilyClass", -// "openTypeOS2SubscriptXOffset", -// "openTypeOS2SubscriptXSize", -// "openTypeOS2SubscriptYOffset", -// "openTypeOS2SubscriptYSize", -// "openTypeOS2SuperscriptXOffset", -// "openTypeOS2SuperscriptXSize", -// "openTypeOS2SuperscriptYOffset", -// "openTypeOS2SuperscriptYSize", -// "openTypeVheaCaretOffset", -// "openTypeVheaCaretSlopeRise", -// "openTypeVheaCaretSlopeRun", -// "openTypeVheaVertTypoLineGap", -// "postscriptDefaultCharacter", -// "postscriptDefaultWidthX", -// "postscriptIsFixedPitch", -// "postscriptNominalWidthX", -// "postscriptSlantAngle", -// "postscriptUniqueID", -// "postscriptWeightName", -// "postscriptWindowsCharacterSet", +// "openTypeGaspRangeRecords", // TODO: This is more complex, please see: https://unifiedfontobject.org/versions/ufo3/fontinfo.plist/#opentype-gasp-table-fields +// "openTypeNameRecords", // TODO: This is more complex, please see: https://unifiedfontobject.org/versions/ufo3/fontinfo.plist/#name-record-format From f27e558495dc67c6174b6c8e7b26ceb39882ecb7 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 12:45:19 +0100 Subject: [PATCH 23/33] Fix typo --- src/fontra/backends/designspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontra/backends/designspace.py b/src/fontra/backends/designspace.py index f6e9e621f..6d819a883 100644 --- a/src/fontra/backends/designspace.py +++ b/src/fontra/backends/designspace.py @@ -2181,8 +2181,8 @@ def updateFontInfoFromFontSource(reader, fontSource): # delete custom data for infoAttr in ufoInfoAttributesToRoundTrip: - customDataKay = f"{ufoInfoPrefix}{infoAttr}" - if customDataKay not in fontSource.customData.keys(): + customDataKey = f"{ufoInfoPrefix}{infoAttr}" + if customDataKey not in fontSource.customData.keys(): value = getattr(fontInfo, infoAttr, None) if value is not None: delattr(fontInfo, infoAttr) From 0105b0c680f044769c5e41e56aab1b24e3192932 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 14:24:25 +0100 Subject: [PATCH 24/33] Fix sorting of font source customData --- src/fontra/views/fontinfo/panel-sources.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 8fe33b221..02272f18f 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -959,11 +959,11 @@ function buildFontCustomDataList(controller, fontSource) { const sortedItems = Object.entries(model); sortedItems.sort( (a, b) => - (customDataNames.indexOf(a[0]) != -1 - ? customDataNames.indexOf(a[0]) + (customDataNames.indexOf(a[0].substring(ufoInfoPrefix.length)) != -1 + ? customDataNames.indexOf(a[0].substring(ufoInfoPrefix.length)) : customDataNames.length) - - (customDataNames.indexOf(b[0]) != -1 - ? customDataNames.indexOf(b[0]) + (customDataNames.indexOf(b[0].substring(ufoInfoPrefix.length)) != -1 + ? customDataNames.indexOf(b[0].substring(ufoInfoPrefix.length)) : customDataNames.length) ); const items = sortedItems?.map(makeItem) || []; From e202d2edd228f970eddc98d9d347e9d3b022ef73 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 15:30:24 +0100 Subject: [PATCH 25/33] Replace NumberArrayFormatter with ArrayFormatter --- src/fontra/client/core/customData.js | 26 ++++++------- src/fontra/client/core/formatters.js | 31 +++++++--------- src/fontra/views/fontinfo/panel-sources.js | 11 +++--- test-js/test-formatters.js | 43 +++++++++++++++++++--- 4 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/fontra/client/core/customData.js b/src/fontra/client/core/customData.js index abc78e91f..ea154cbf4 100644 --- a/src/fontra/client/core/customData.js +++ b/src/fontra/client/core/customData.js @@ -1,6 +1,6 @@ import { + ArrayFormatter, BooleanFormatter, - NumberArrayFormatter, PanoseArrayFormatter, _NumberFormatter, } from "./formatters.js"; @@ -79,25 +79,25 @@ export const customDataNameMapping = { openTypeOS2WeightClass: { default: () => 400, formatter: _NumberFormatter }, openTypeOS2WidthClass: { default: () => 5, formatter: _NumberFormatter }, openTypeHeadCreated: { default: getCreatedDefault }, // The timezone is UTC. - openTypeOS2Selection: { default: () => [], formatter: NumberArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 - openTypeOS2Type: { default: () => [3], formatter: NumberArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 + openTypeOS2Selection: { default: () => [], formatter: ArrayFormatter }, // 7 = Use Typo Metrics, 8 = has WWS name, https://github.com/fonttools/fonttools/blob/598b974f87f35972da24e96e45bd0176d18930a0/Lib/fontTools/ufoLib/__init__.py#L1889 + openTypeOS2Type: { default: () => [3], formatter: ArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 openTypeOS2Panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], formatter: PanoseArrayFormatter, }, // default: sans-serif - openTypeOS2FamilyClass: { default: () => [8, 0], formatter: NumberArrayFormatter }, // Class ID 8 = Sans Serif, Subclass ID = 0: No Classification - openTypeOS2UnicodeRanges: { default: () => [], formatter: NumberArrayFormatter }, - openTypeOS2CodePageRanges: { default: () => [], formatter: NumberArrayFormatter }, + openTypeOS2FamilyClass: { default: () => [8, 0], formatter: ArrayFormatter }, // Class ID 8 = Sans Serif, Subclass ID = 0: No Classification + openTypeOS2UnicodeRanges: { default: () => [], formatter: ArrayFormatter }, + openTypeOS2CodePageRanges: { default: () => [], formatter: ArrayFormatter }, // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf - postscriptBlueValues: { default: () => [], formatter: NumberArrayFormatter }, - postscriptOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, - postscriptFamilyBlues: { default: () => [], formatter: NumberArrayFormatter }, - postscriptFamilyOtherBlues: { default: () => [], formatter: NumberArrayFormatter }, + postscriptBlueValues: { default: () => [], formatter: ArrayFormatter }, + postscriptOtherBlues: { default: () => [], formatter: ArrayFormatter }, + postscriptFamilyBlues: { default: () => [], formatter: ArrayFormatter }, + postscriptFamilyOtherBlues: { default: () => [], formatter: ArrayFormatter }, postscriptBlueScale: { default: () => 0.039625, formatter: _NumberFormatter }, postscriptBlueShift: { default: () => 1, formatter: _NumberFormatter }, postscriptBlueFuzz: { default: () => 1, formatter: _NumberFormatter }, - postscriptStemSnapH: { default: () => [], formatter: NumberArrayFormatter }, - postscriptStemSnapV: { default: () => [], formatter: NumberArrayFormatter }, + postscriptStemSnapH: { default: () => [], formatter: ArrayFormatter }, + postscriptStemSnapV: { default: () => [], formatter: ArrayFormatter }, postscriptForceBold: { default: () => false, formatter: BooleanFormatter }, // PostScript Specific Data // postscriptFontName // NOTE: not in ufoInfoAttributesToRoundTrip @@ -132,7 +132,7 @@ export const customDataNameMapping = { openTypeOS2SuperscriptYOffset: { default: () => 0, formatter: _NumberFormatter }, // OpenType OS/2 Table Fields openTypeHeadLowestRecPPEM: { default: () => 6, formatter: _NumberFormatter }, // Smallest readable size in pixels. - openTypeHeadFlags: { default: () => [], formatter: NumberArrayFormatter }, + openTypeHeadFlags: { default: () => [], formatter: ArrayFormatter }, }; // TODO: Based on ufoInfoAttributesToRoundTrip (designspace.py) diff --git a/src/fontra/client/core/formatters.js b/src/fontra/client/core/formatters.js index fcde5a3b5..3344cd58c 100644 --- a/src/fontra/client/core/formatters.js +++ b/src/fontra/client/core/formatters.js @@ -36,25 +36,22 @@ export const BooleanFormatter = { }; export const ArrayFormatter = { - toString: (value) => { - if (Array.isArray(value)) { - return value.toString(); - } - }, - fromString: (value) => { - const array = JSON.parse("[" + value + "]"); - if (Array.isArray(array)) { - return { value: array }; - } else { + toString: (value, arrayLength) => { + if (!Array.isArray(value)) { return { error: "not an array" }; } + if (arrayLength && value.length != arrayLength) { + return { error: `array length must be ${arrayLength}` }; + } + return value.toString(); }, -}; - -export const NumberArrayFormatter = { - toString: (value) => value.toString(), fromString: (value, arrayLength) => { - const array = JSON.parse("[" + value + "]"); + let array = []; + try { + array = JSON.parse("[" + value + "]"); + } catch (e) { + return { error: e }; + } if (Array.isArray(array)) { if (arrayLength && array.length != arrayLength) { return { error: `array length must be ${arrayLength}` }; @@ -67,6 +64,6 @@ export const NumberArrayFormatter = { }; export const PanoseArrayFormatter = { - toString: (value) => NumberArrayFormatter.toString(value), - fromString: (value) => NumberArrayFormatter.fromString(value, 10), + toString: (value) => ArrayFormatter.toString(value, 10), + fromString: (value) => ArrayFormatter.fromString(value, 10), }; diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 02272f18f..aeb66fe01 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -695,14 +695,15 @@ class SourceBox extends HTMLElement { continue; } const formatter = customDataNameMapping[key]?.formatter || DefaultFormatter; - const value = formatter.fromString(item["value"]).value; - if (value !== undefined) { - source.customData[`${ufoInfoPrefix}${key}`] = value; - } else { + const result = formatter.fromString(item["value"]); + if (result.value == undefined) { + const msg = result.error ? ` (${result.error})` : ""; message( translate("sources.dialog.cannot-edit-source.title"), - `"${key}" invalid value: ${item["value"]}` + `"${key}" invalid value: ${item["value"]}${msg}` ); + } else { + source.customData[`${ufoInfoPrefix}${key}`] = result.value; } } }, `edit customData`); // TODO: translation diff --git a/test-js/test-formatters.js b/test-js/test-formatters.js index f7c9c4cca..8c8a08bba 100644 --- a/test-js/test-formatters.js +++ b/test-js/test-formatters.js @@ -2,7 +2,7 @@ import { expect } from "chai"; import { ArrayFormatter, BooleanFormatter, - NumberArrayFormatter, + PanoseArrayFormatter, _NumberFormatter, } from "../src/fontra/client/core/formatters.js"; @@ -40,6 +40,7 @@ describe("ArrayFormatter", () => { ["1,2,3,4", [1, 2, 3, 4]], ["1, 2,3,4", [1, 2, 3, 4]], ["", []], + ["Hello", undefined], ], (testData) => { const [input, expectedResult] = testData; @@ -54,8 +55,8 @@ describe("ArrayFormatter", () => { [ [[1, 2, 3, 4], "1,2,3,4"], [[], ""], - [true, undefined], - [new Set([1, 2, 3]), undefined], + [true, { error: "not an array" }], + [new Set([1, 2, 3]), { error: "not an array" }], ], (testData) => { const [input, expectedResult] = testData; @@ -64,9 +65,9 @@ describe("ArrayFormatter", () => { ); }); -describe("NumberArrayFormatter", () => { +describe("ArrayFormatter", () => { parametrize( - "NumberArrayFormatter fromString tests", + "ArrayFormatter with arrayLength fromString tests", [ ["1,2,3,4", [1, 2, 3, 4], 4], ["1, 2,3, 4", [1, 2, 3, 4], 4], @@ -75,13 +76,43 @@ describe("NumberArrayFormatter", () => { ], (testData) => { const [input, expectedResult, arrayLength] = testData; - expect(NumberArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( + expect(ArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( expectedResult ); } ); }); +describe("PanoseArrayFormatter", () => { + parametrize( + "PanoseArrayFormatter fromString tests", + [ + ["1,2,3,4,5,6,7,8,9,10", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], + ["1,2,3,4,5,6,7,8,9", undefined], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(PanoseArrayFormatter.fromString(input).value).to.deep.equal( + expectedResult + ); + } + ); +}); + +describe("PanoseArrayFormatter", () => { + parametrize( + "PanoseArrayFormatter toString tests", + [ + [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "1,2,3,4,5,6,7,8,9,10"], + [[1, 2, 3, 4, 5, 6, 7, 8, 9], { error: "array length must be 10" }], + ], + (testData) => { + const [input, expectedResult] = testData; + expect(PanoseArrayFormatter.toString(input)).to.deep.equal(expectedResult); + } + ); +}); + describe("BooleanFormatter", () => { parametrize( "BooleanFormatter fromString tests", From a5a78647c86c7ab9f057425c40a6a7eed2aa3de8 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 15:39:54 +0100 Subject: [PATCH 26/33] Use .slice() instead of .substring() --- src/fontra/views/fontinfo/panel-sources.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index aeb66fe01..1d57b7848 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -930,7 +930,7 @@ function buildFontCustomDataList(controller, fontSource) { const makeItem = ([key, value]) => { const keyDisplayed = key.startsWith(ufoInfoPrefix) - ? key.substring(ufoInfoPrefix.length) + ? key.slice(ufoInfoPrefix.length) : key; const item = new ObservableController({ key: keyDisplayed, value: value }); item.addListener((event) => { @@ -960,11 +960,11 @@ function buildFontCustomDataList(controller, fontSource) { const sortedItems = Object.entries(model); sortedItems.sort( (a, b) => - (customDataNames.indexOf(a[0].substring(ufoInfoPrefix.length)) != -1 - ? customDataNames.indexOf(a[0].substring(ufoInfoPrefix.length)) + (customDataNames.indexOf(a[0].slice(ufoInfoPrefix.length)) != -1 + ? customDataNames.indexOf(a[0].slice(ufoInfoPrefix.length)) : customDataNames.length) - - (customDataNames.indexOf(b[0].substring(ufoInfoPrefix.length)) != -1 - ? customDataNames.indexOf(b[0].substring(ufoInfoPrefix.length)) + (customDataNames.indexOf(b[0].slice(ufoInfoPrefix.length)) != -1 + ? customDataNames.indexOf(b[0].slice(ufoInfoPrefix.length)) : customDataNames.length) ); const items = sortedItems?.map(makeItem) || []; From c54dcacd038e7aad481ae5697912fcf6fda01628 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 15:59:39 +0100 Subject: [PATCH 27/33] Rename customData.js to font-info-data.js --- src/fontra/client/core/{customData.js => font-info-data.js} | 0 src/fontra/views/fontinfo/panel-sources.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/fontra/client/core/{customData.js => font-info-data.js} (100%) diff --git a/src/fontra/client/core/customData.js b/src/fontra/client/core/font-info-data.js similarity index 100% rename from src/fontra/client/core/customData.js rename to src/fontra/client/core/font-info-data.js diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index 1d57b7848..f0887fad2 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -1,6 +1,6 @@ import { doPerformAction, getActionIdentifierFromKeyEvent } from "../core/actions.js"; import { recordChanges } from "../core/change-recorder.js"; -import { customDataNameMapping } from "../core/customData.js"; +import { customDataNameMapping } from "../core/font-info-data.js"; import * as html from "../core/html-utils.js"; import { addStyleSheet } from "../core/html-utils.js"; import { translate } from "../core/localization.js"; From 03b70453bf2cb3757fa91385a30eed37237668d1 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 16:04:50 +0100 Subject: [PATCH 28/33] Fix typo --- src/fontra/client/core/font-info-data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fontra/client/core/font-info-data.js b/src/fontra/client/core/font-info-data.js index ea154cbf4..4738871b7 100644 --- a/src/fontra/client/core/font-info-data.js +++ b/src/fontra/client/core/font-info-data.js @@ -45,7 +45,7 @@ function getCreatedDefault() { } export const customDataNameMapping = { - // verticl metrics values + // vertical metrics values openTypeHheaAscender: { default: getAscenderDefault, formatter: _NumberFormatter }, openTypeHheaDescender: { default: getDescenderDefault, formatter: _NumberFormatter }, openTypeHheaLineGap: { default: () => 0, formatter: _NumberFormatter }, From 071e1ccd72d38ebb667a0147df4e6387db09d9fc Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 16:26:56 +0100 Subject: [PATCH 29/33] Remove getDescenderWinDefault --- src/fontra/client/core/font-info-data.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/fontra/client/core/font-info-data.js b/src/fontra/client/core/font-info-data.js index 4738871b7..5349bb883 100644 --- a/src/fontra/client/core/font-info-data.js +++ b/src/fontra/client/core/font-info-data.js @@ -13,10 +13,6 @@ function getDescenderDefault(fontSource = undefined) { return fontSource.lineMetricsHorizontalLayout.descender.value || -200; } -function getDescenderWinDefault(fontSource = undefined) { - return fontSource.lineMetricsHorizontalLayout.descender.value * -1 || 200; -} - function getFamilyNameDefault(fontSource = undefined) { return fontSource.familyName || "Family Name"; } @@ -25,7 +21,7 @@ function getSubfamilyNameDefault(fontSource = undefined) { return fontSource.name || "Subfamily Name"; } -function getstrikeoutPositionDefault(fontSource = undefined) { +function getStrikeoutPositionDefault(fontSource = undefined) { return fontSource.lineMetricsHorizontalLayout.ascender.value / 2 || 250; } @@ -57,13 +53,13 @@ export const customDataNameMapping = { openTypeOS2TypoLineGap: { default: () => 0, formatter: _NumberFormatter }, openTypeOS2WinAscent: { default: getAscenderDefault, formatter: _NumberFormatter }, openTypeOS2WinDescent: { - default: getDescenderWinDefault, + default: (fontSource) => getDescenderDefault(fontSource) * -1, formatter: _NumberFormatter, }, postscriptUnderlinePosition: { default: () => -100, formatter: _NumberFormatter }, postscriptUnderlineThickness: { default: () => 50, formatter: _NumberFormatter }, openTypeOS2StrikeoutPosition: { - default: getstrikeoutPositionDefault, + default: getStrikeoutPositionDefault, formatter: _NumberFormatter, }, openTypeOS2StrikeoutSize: { default: () => 50, formatter: _NumberFormatter }, @@ -108,7 +104,7 @@ export const customDataNameMapping = { postscriptIsFixedPitch: { default: () => false, formatter: BooleanFormatter }, // Indicates if the font is monospaced. postscriptDefaultWidthX: { default: () => 0, formatter: _NumberFormatter }, postscriptNominalWidthX: { default: () => 0, formatter: _NumberFormatter }, - postscriptDefaultCharacter: { default: () => "glyphName" }, // The name of the glyph that should be used as the default character in PFM files. + postscriptDefaultCharacter: { default: () => "glyphName" }, // The name of the glyph that should be used as the default character in PFM files. postscriptWindowsCharacterSet: { default: () => 0, formatter: _NumberFormatter }, // OpenType vhea Table Fields // openTypeVheaVertTypoAscender // NOTE: not in ufoInfoAttributesToRoundTrip From 67ce5d083e8a4b33f1fbfb33558e6e475ab16fbf Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 16:57:44 +0100 Subject: [PATCH 30/33] Replace PanoseArrayFormatter with FixedLengthArrayFormatter + adjust unittests --- src/fontra/client/core/font-info-data.js | 13 ++++--- src/fontra/client/core/formatters.js | 8 ++-- test-js/test-formatters.js | 48 +++++++++--------------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/fontra/client/core/font-info-data.js b/src/fontra/client/core/font-info-data.js index 5349bb883..6748b9b52 100644 --- a/src/fontra/client/core/font-info-data.js +++ b/src/fontra/client/core/font-info-data.js @@ -1,7 +1,7 @@ import { ArrayFormatter, BooleanFormatter, - PanoseArrayFormatter, + FixedLengthArrayFormatter, _NumberFormatter, } from "./formatters.js"; @@ -79,9 +79,12 @@ export const customDataNameMapping = { openTypeOS2Type: { default: () => [3], formatter: ArrayFormatter }, // https://github.com/googlefonts/glyphsLib/blob/c4db6b981d577f456d64ebe9993818770e170454/Lib/glyphsLib/builder/custom_params.py#L1166 openTypeOS2Panose: { default: () => [2, 11, 5, 2, 4, 5, 4, 2, 2, 4], - formatter: PanoseArrayFormatter, + formatter: FixedLengthArrayFormatter(10), }, // default: sans-serif - openTypeOS2FamilyClass: { default: () => [8, 0], formatter: ArrayFormatter }, // Class ID 8 = Sans Serif, Subclass ID = 0: No Classification + openTypeOS2FamilyClass: { + default: () => [8, 0], + formatter: FixedLengthArrayFormatter(2), + }, // Class ID 8 = Sans Serif, Subclass ID = 0: No Classification openTypeOS2UnicodeRanges: { default: () => [], formatter: ArrayFormatter }, openTypeOS2CodePageRanges: { default: () => [], formatter: ArrayFormatter }, // Postscript Font Level Hints, // https://adobe-type-tools.github.io/font-tech-notes/pdfs/T1_SPEC.pdf @@ -107,8 +110,8 @@ export const customDataNameMapping = { postscriptDefaultCharacter: { default: () => "glyphName" }, // The name of the glyph that should be used as the default character in PFM files. postscriptWindowsCharacterSet: { default: () => 0, formatter: _NumberFormatter }, // OpenType vhea Table Fields - // openTypeVheaVertTypoAscender // NOTE: not in ufoInfoAttributesToRoundTrip - // openTypeVheaVertTypoDescender // NOTE: not in ufoInfoAttributesToRoundTrip + // openTypeVheaVertTypoAscender // NOTE: part of lineMetricsVerMapping + // openTypeVheaVertTypoDescender // NOTE: part of lineMetricsVerMapping openTypeVheaVertTypoLineGap: { default: () => 0, formatter: _NumberFormatter }, openTypeVheaCaretSlopeRise: { default: () => 0, formatter: _NumberFormatter }, openTypeVheaCaretSlopeRun: { default: () => 0, formatter: _NumberFormatter }, diff --git a/src/fontra/client/core/formatters.js b/src/fontra/client/core/formatters.js index 3344cd58c..38e03f8cf 100644 --- a/src/fontra/client/core/formatters.js +++ b/src/fontra/client/core/formatters.js @@ -63,7 +63,7 @@ export const ArrayFormatter = { }, }; -export const PanoseArrayFormatter = { - toString: (value) => ArrayFormatter.toString(value, 10), - fromString: (value) => ArrayFormatter.fromString(value, 10), -}; +export const FixedLengthArrayFormatter = (arrayLength) => ({ + toString: (value) => ArrayFormatter.toString(value, arrayLength), + fromString: (value) => ArrayFormatter.fromString(value, arrayLength), +}); diff --git a/test-js/test-formatters.js b/test-js/test-formatters.js index 8c8a08bba..c8ef1dd11 100644 --- a/test-js/test-formatters.js +++ b/test-js/test-formatters.js @@ -2,7 +2,7 @@ import { expect } from "chai"; import { ArrayFormatter, BooleanFormatter, - PanoseArrayFormatter, + FixedLengthArrayFormatter, _NumberFormatter, } from "../src/fontra/client/core/formatters.js"; @@ -65,54 +65,40 @@ describe("ArrayFormatter", () => { ); }); -describe("ArrayFormatter", () => { +describe("FixedLengthArrayFormatter", () => { parametrize( - "ArrayFormatter with arrayLength fromString tests", + "FixedLengthArrayFormatter fromString tests", [ - ["1,2,3,4", [1, 2, 3, 4], 4], - ["1, 2,3, 4", [1, 2, 3, 4], 4], - ["1, 2,3,4", undefined, 3], - ["", [], 0], + [10, "1,2,3,4,5,6,7,8,9,10", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], + [10, "1,2,3,4,5,6,7,8,9", undefined], + [2, "8,0", [8, 0]], ], (testData) => { - const [input, expectedResult, arrayLength] = testData; - expect(ArrayFormatter.fromString(input, arrayLength).value).to.deep.equal( - expectedResult - ); + const [arrayLength, input, expectedResult] = testData; + expect( + FixedLengthArrayFormatter(arrayLength).fromString(input).value + ).to.deep.equal(expectedResult); } ); }); -describe("PanoseArrayFormatter", () => { +describe("FixedLengthArrayFormatter", () => { parametrize( - "PanoseArrayFormatter fromString tests", + "FixedLengthArrayFormatter toString tests", [ - ["1,2,3,4,5,6,7,8,9,10", [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], - ["1,2,3,4,5,6,7,8,9", undefined], + [10, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "1,2,3,4,5,6,7,8,9,10"], + [10, [1, 2, 3, 4, 5, 6, 7, 8, 9], { error: "array length must be 10" }], + [2, [8, 0], "8,0"], ], (testData) => { - const [input, expectedResult] = testData; - expect(PanoseArrayFormatter.fromString(input).value).to.deep.equal( + const [arrayLength, input, expectedResult] = testData; + expect(FixedLengthArrayFormatter(arrayLength).toString(input)).to.deep.equal( expectedResult ); } ); }); -describe("PanoseArrayFormatter", () => { - parametrize( - "PanoseArrayFormatter toString tests", - [ - [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "1,2,3,4,5,6,7,8,9,10"], - [[1, 2, 3, 4, 5, 6, 7, 8, 9], { error: "array length must be 10" }], - ], - (testData) => { - const [input, expectedResult] = testData; - expect(PanoseArrayFormatter.toString(input)).to.deep.equal(expectedResult); - } - ); -}); - describe("BooleanFormatter", () => { parametrize( "BooleanFormatter fromString tests", From 6eef48fc06b7c83e902b2d148daef76d4132cb4d Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Tue, 25 Feb 2025 17:20:01 +0100 Subject: [PATCH 31/33] Remove getFamilyNameDefault, because fontSource has no 'familyName'. --- src/fontra/client/core/font-info-data.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/fontra/client/core/font-info-data.js b/src/fontra/client/core/font-info-data.js index 6748b9b52..8bbce4ab4 100644 --- a/src/fontra/client/core/font-info-data.js +++ b/src/fontra/client/core/font-info-data.js @@ -13,10 +13,6 @@ function getDescenderDefault(fontSource = undefined) { return fontSource.lineMetricsHorizontalLayout.descender.value || -200; } -function getFamilyNameDefault(fontSource = undefined) { - return fontSource.familyName || "Family Name"; -} - function getSubfamilyNameDefault(fontSource = undefined) { return fontSource.name || "Subfamily Name"; } @@ -66,10 +62,10 @@ export const customDataNameMapping = { // name table entries openTypeNameUniqueID: { default: () => "uniqueID Name ID 3" }, // Name ID 3 openTypeNameVersion: { default: () => "Version 1.0" }, // Name ID 7 - openTypeNamePreferredFamilyName: { default: getFamilyNameDefault }, // Name ID 16 + openTypeNamePreferredFamilyName: { default: "Family Name" }, // Name ID 16 openTypeNamePreferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 openTypeNameCompatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 - openTypeNameWWSFamilyName: { default: getFamilyNameDefault }, // Name ID 21 + openTypeNameWWSFamilyName: { default: "Family Name" }, // Name ID 21 openTypeNameWWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22 // misc openTypeOS2WeightClass: { default: () => 400, formatter: _NumberFormatter }, From 4a39970c37bf263a0527964b085caf48e9973862 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 26 Feb 2025 13:49:27 +0100 Subject: [PATCH 32/33] Edit comment --- src/fontra/views/fontinfo/panel-sources.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fontra/views/fontinfo/panel-sources.js b/src/fontra/views/fontinfo/panel-sources.js index f0887fad2..b2c76749c 100644 --- a/src/fontra/views/fontinfo/panel-sources.js +++ b/src/fontra/views/fontinfo/panel-sources.js @@ -684,7 +684,7 @@ class SourceBox extends HTMLElement { for (const item of event.newValue) { const key = item["key"]; if (key === "attributeName") { - // Skip this, so people can edit this placeholder it. + // Skip this, so people can edit this placeholder. continue; } if (!customDataNameMapping[key]) { From 8a3d777652d861c8010f17184e98b9a8dd5918f4 Mon Sep 17 00:00:00 2001 From: Olli Meier Date: Wed, 26 Feb 2025 17:41:27 +0100 Subject: [PATCH 33/33] customDataNameMapping 'default' must be function --- src/fontra/client/core/font-info-data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fontra/client/core/font-info-data.js b/src/fontra/client/core/font-info-data.js index 8bbce4ab4..aa5d1f25c 100644 --- a/src/fontra/client/core/font-info-data.js +++ b/src/fontra/client/core/font-info-data.js @@ -62,10 +62,10 @@ export const customDataNameMapping = { // name table entries openTypeNameUniqueID: { default: () => "uniqueID Name ID 3" }, // Name ID 3 openTypeNameVersion: { default: () => "Version 1.0" }, // Name ID 7 - openTypeNamePreferredFamilyName: { default: "Family Name" }, // Name ID 16 + openTypeNamePreferredFamilyName: { default: () => "Family Name" }, // Name ID 16 openTypeNamePreferredSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 17 openTypeNameCompatibleFullName: { default: () => "Compatible Full Name" }, // Name ID 18 - openTypeNameWWSFamilyName: { default: "Family Name" }, // Name ID 21 + openTypeNameWWSFamilyName: { default: () => "Family Name" }, // Name ID 21 openTypeNameWWSSubfamilyName: { default: getSubfamilyNameDefault }, // Name ID 22 // misc openTypeOS2WeightClass: { default: () => 400, formatter: _NumberFormatter },