googlefonts / googlefonts/fontc

fontc drops point axes with non-identity user-to-design mappings

Open
#1,990 2 comments 0 reactions 0 assignees View on GitHub
crater
Dominant language
Rust
Stars
193
Forks
21
Avg merge
1d 20h
Merged PRs (30d)
60

Description

cc @anthrotype: this is claude's reasoning about a particular diff in Unna.glyphs, but I don't feel like I fully understand the issue. Specifically, it isn't obvious to me why it is useful to had a `wdth` axis at all in this font, if it isn't variable? Fontmake is adding an extra axis, which has some binary and complexity costs, and that axis isn't doing anything. Is that helpful?

claude's output below, which is assuming that fontmake is right:

## Reproduction

```bash
python3 -m ttx_diff 'https://github.com/Omnibus-Type/Unna?826be2abeb#sources/Unna.glyphs'
```

Affected tables: `fvar`, `STAT`, `avar`, `HVAR`, `MVAR`, `name`

## Summary

When a Glyphs source has an axis where all masters share the same design-space position but a non-identity user-to-design mapping exists, fontc drops the axis entirely. fontmake correctly emits it as a point axis with the mapped user-space coordinates.

## Details

The [Unna](https://github.com/Omnibus-Type/Unna) font is a Glyphs v2 source with 2 masters (Regular and Bold). Both masters sit at `widthValue=5`. The font has no explicit "Axes" custom parameter, so it gets the default axes (wght, wdth, custom).

Instance widthClass values produce a non-identity `wdth` mapping: user 100 → design 5. Despite there being no *variation* along this axis, the mapping is semantically meaningful — it defines the user-space coordinate system. The correct output is a point axis in fvar with `min=100, default=100, max=100` and an avar mapping entry `(100, 5)`.

fontc omits the axis completely.

### fontc code path

In [`glyphs2fontir/src/toir.rs:203-205`](https://github.com/googlefonts/fontc/blob/6b57ed8c/glyphs2fontir/src/toir.rs#L203-L205), the `has_non_identity_mapping` check has three conditions:

```rust
let has_non_identity_mapping = font.axis_mappings.contains(&axis.name)
&& !font.axis_mappings.get(&axis.name).unwrap().is_identity()
&& min != max;
```

For `wdth`, both masters have `widthValue=5`, so `min == max == DesignCoord(5)`. The third condition `min != max` is false, making the entire expression false. The code falls through to the unmapped branch ([line 220-225](https://github.com/googlefonts/fontc/blob/6b57ed8c/glyphs2fontir/src/toir.rs#L220-L225)):

```rust
let min = UserCoord::new(min.into_inner()); // 5.0
let max = UserCoord::new(max.into_inner()); // 5.0
let default = UserCoord::new(default.into_inner()); // 5.0
CoordConverter::unmapped(min, default, max)
```

The axis becomes `min=5, default=5, max=5` in user space (raw design units, not the CSS percentage scale). Then in [`fontir/src/ir/static_metadata.rs:400`](https://github.com/googlefonts/fontc/blob/6b57ed8c/fontir/src/ir/static_metadata.rs#L400):

```rust
let variable_axes: Axes = axes.iter().filter(|a| !a.is_point()).cloned().collect();
```

This filters out all point axes. The wdth axis is gone from fvar, STAT, avar, and name.

### fontmake code path

In [`glyphsLib/builder/axes.py:288-293`](https://github.com/googlefonts/glyphsLib/blob/6f243c1f/Lib/glyphsLib/builder/axes.py#L288-L293), the axis inclusion condition is:

```python
if (
minimum < maximum
or minimum != axis_def.default_user_loc
or not is_identity_map
or axis_wanted
):
```

The `not is_identity_map` condition catches this case. The mapping `{100: 5}` is non-identity, so the axis IS included despite `minimum == maximum`. The axis is emitted with `min=100, default=100, max=100` and a mapping entry `(100, 5)`.

### Why the `min != max` guard is wrong

The comment above the check ([lines 199-202](https://github.com/googlefonts/fontc/blob/6b57ed8c/glyphs2fontir/src/toir.rs#L199-L202)) explains the intent:

```rust
// If all masters sit at the same position on this axis, the mapping is
// meaningless and there's no variation to map. Treat as unmapped.
```

This conflates two distinct purposes of the axis mapping:

1. **Mapping variation deltas** between coordinate systems — this is indeed meaningless when there's no variation.
2. **Defining the user-space coordinate system** for fvar/STAT — this is meaningful regardless of variation. A point axis at `wdth=100` (CSS percentage) is semantically different from one at `wdth=5` (internal design units).

### Downstream effects

| Table | fontc | fontmake |
|-------|-------|----------|
| fvar | 1 axis (wght) | 2 axes (wght, wdth) |
| STAT | DesignAxisCount=1 | DesignAxisCount=2 |
| avar | no wdth segment | wdth identity segment |
| HVAR/MVAR | RegionAxisCount=1 | RegionAxisCount=2 |
| name | no "Width" axis name | has "Width" axis name |

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.