Skip to content

designspace v5 (take 2) - #418

Merged
cmyr merged 3 commits into
plist-data-loadfrom
designspace-v5
Sep 24, 2026
Merged

cmyr merged 3 commits into
plist-data-loadfrom
designspace-v5

Conversation

@cmyr

@cmyr cmyr commented Sep 22, 2026

Copy link
Copy Markdown
Member

Adds the v5 elements from #416: elidedfallbackname, axis STAT labels, top-level location labels, variable-fonts with axis-subsets, localised source family names, and instance location labels and localised names. The same structs handle 4.x and 5.x; the only API breakage is new pub fields on existing structs.

An axis-subset is modelled as a name plus SubsetValue (Whole, Tent or Discrete), parsed via a raw struct rather than an untagged enum, which quick-xml can't handle. On save, format is bumped to the minimum version the content needs, as fontTools does.

Based on #340 by @madig; the test file is fontTools' test_v5.designspace plus his additions.

cmyr and others added 2 commits September 22, 2026 14:21
Adds the v5 elements: elidedfallbackname, axis STAT labels,
top-level location labels, variable-fonts with axis-subsets, localised
source family names, and instance location labels and localised names.
The same structs handle 4.x and 5.x; the only API breakage is new pub
fields on existing structs.

An axis-subset is modelled as a name plus SubsetValue (Whole, Tent or
Discrete), parsed via a raw struct rather than an untagged enum, which
quick-xml can't handle. On save, format is bumped to the minimum
version the content needs, as fontTools does.
@madig

madig commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Thank you for working on this! This LGTM. I added a tiny change to make sure the DSv5 test data exercises two probably seldom used attributes.

About the various libs that have been added to element structs, I had the thought of extracting and parsing the public.fontInfo key into a proper FontInfo struct, accessible next to an element's lib field (so e.g. VariableFont would have a lib and font_info field), but I'm not sure if that's a good idea or should be left to the compiler... It would at least mean that malformed font info data leads to failing to load the DS.

Comment thread src/designspace.rs Outdated
(None, None, None) => SubsetValue::Full,
_ => {
return Err(AxisSubsetError(
"axis-subset element must have min/max/default values or none at all",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so.. this error matches fontTools designspace reader
https://github.com/fonttools/fonttools/blob/718b61b552841de3964ba5b691fcf6102a2ebb3c/Lib/fontTools/designspaceLib/__init__.py#L2377

however the spec https://fonttools.readthedocs.io/en/latest/designspaceLib/xml.html#axis-subset-element explicitly makes userminimum, usermaximum and userdefault individually optional. The missing bounds come from the parent axis, whereas an omitted default should use the parent default clamped into the subset, something like max(subset_min, min(parent_default, subset_max)).

The problem is fontTools designspace writer is inconsistent with its reader: e.g. create an axis Weight 100/400/900 and a variable-font RangeAxisSubsetDescriptor(name=”Weight”, userMinimum=700), then serialize. fontTools writes:

<axis-subset name="Weight" userminimum="700"/>

which both readers reject.

Since we are adding this here, might be worth following the spec and update fonttools impl separately?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fwiw I opened fonttools/fonttools#4205 for the fontTools side.

For the model here, what if we fold Full into Range and make all three fields optional?

    pub enum SubsetValue {
        /// Missing values default to the parent axis (default clamped to the subset).
        Range { minimum: Option<f64>, default: Option<f64>, maximum: Option<f64> },
        Discrete(f64),
    }

That's how fontTools models it too (the whole axis is just a RangeAxisSubsetDescriptor with no bounds set), and it would avoid having two ways to spell the whole axis (Full vs an all-None Range). The TryFrom only needs to reject uservalue mixed with any of the range attributes. The all-None Range can be the #[default], maybe with an is_full() helper so we don't lose the readability of Full.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, that sounds good!

@anthrotype anthrotype left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but see my comments above on partial axis-subset ranges

Make the axis-subset range values individually optional, as the spec
allows, and fold SubsetValue::Full into Range, per
#418 (comment)
@cmyr
cmyr merged commit 471a882 into plist-data-load Sep 24, 2026
5 checks passed
@cmyr
cmyr deleted the designspace-v5 branch September 24, 2026 15:18
@cmyr
cmyr restored the designspace-v5 branch September 24, 2026 22:11
cmyr added a commit that referenced this pull request Sep 25, 2026
Make the axis-subset range values individually optional, as the spec
allows, and fold SubsetValue::Full into Range, per
#418 (comment)
cmyr added a commit that referenced this pull request Sep 25, 2026
Make the axis-subset range values individually optional, as the spec
allows, and fold SubsetValue::Full into Range, per
#418 (comment)
@madig

madig commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

@cmyr @anthrotype What do you think of the font_info field idea above? I'm unsure if it's a good idea or not.

@anthrotype

Copy link
Copy Markdown
Collaborator

extracting and parsing the public.fontInfo key into a proper FontInfo struct, accessible next to an element's lib field (so e.g. VariableFont would have a lib and font_info field)

I'd keep it out of the structs. With both a raw lib entry and a typed font_info field, it isn't clear which one wins when norad writes the file back. If anything, you could add an accessor that parses on demand, e.g. something like fn font_info(&self) -> Result<Option<FontInfo>, _> on DesignSpaceDocument, Instance and VariableFont.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants