The IR represents color layer fills as resolved Color values (PaintSolid { color: Option<Color> }), not palette indices. Both frontends (glyphs2fontir and soon ufo2fontir via #1904) resolve palette indices to colors by looking up the first palette 0, and the backend reconstructs indices via ColorPalettes::index_of() which similarly searches palette 0 for a matching color.
This round-trip is lossy if palette 0 contains duplicate color values at different indices. The CPAL spec does not require entries to be unique. A font may intentionally use the same color at two indices in palette 0 if they diverge in alternate palettes (e.g., a "monochrome" palette where distinct colors collapse).
In that case, index_of returns the first match, silently remapping the original index.
I propose we fix this by storing palette indices in the IR instead of resolved colors. PaintSolid would hold an Option<u16> (palette index, None for 0xFFFF foreground) instead of Option<Color>. Similarly for gradient ColorStop.
This would move palette construction entirely into the frontends:
- UFO: already has indices in the source, pass through directly
- Glyphs with explicit (COLRv0)
colorPalette layer attribute: already has indices, pass through
- Glyphs (COLRv1) with inline colors: frontend builds the palette from unique colors, then maps each color -> index. Unambiguous since the palette itself is derived from those exact colors.
The backend would consume indices directly, eliminating the lossy index_of color matching.
A trade off is the Glyphs ColorGlyphsWork which currently has no dependency on ColorPaletteWork would need to read the palette from context to resolve inline colors -> indices, adding a work graph dependency. Negligible perf impact since palette construction is fast.
Alternatives are validating that palette 0 has no duplicate colors and error early (rejects spec-valid fonts) or accept the limitation and document it (pragmatic given real-world fonts with duplicate palette 0 entries are rare).
The IR represents color layer fills as resolved
Colorvalues (PaintSolid { color: Option<Color> }), not palette indices. Both frontends (glyphs2fontir and soon ufo2fontir via #1904) resolve palette indices to colors by looking up the first palette 0, and the backend reconstructs indices viaColorPalettes::index_of()which similarly searches palette 0 for a matching color.This round-trip is lossy if palette 0 contains duplicate color values at different indices. The CPAL spec does not require entries to be unique. A font may intentionally use the same color at two indices in palette 0 if they diverge in alternate palettes (e.g., a "monochrome" palette where distinct colors collapse).
In that case,
index_ofreturns the first match, silently remapping the original index.I propose we fix this by storing palette indices in the IR instead of resolved colors.
PaintSolidwould hold anOption<u16>(palette index,Nonefor 0xFFFF foreground) instead ofOption<Color>. Similarly for gradientColorStop.This would move palette construction entirely into the frontends:
colorPalettelayer attribute: already has indices, pass throughThe backend would consume indices directly, eliminating the lossy
index_ofcolor matching.A trade off is the Glyphs
ColorGlyphsWorkwhich currently has no dependency onColorPaletteWorkwould need to read the palette from context to resolve inline colors -> indices, adding a work graph dependency. Negligible perf impact since palette construction is fast.Alternatives are validating that palette 0 has no duplicate colors and error early (rejects spec-valid fonts) or accept the limitation and document it (pragmatic given real-world fonts with duplicate palette 0 entries are rare).