Skip to content

[macOS] Honor COLR/CPAL palette selection on CoreText (SKTypeface.Clone palette)#4265

Draft
mattleibow wants to merge 2 commits into
mainfrom
mattleibow-coretext-colr-palette-fix
Draft

[macOS] Honor COLR/CPAL palette selection on CoreText (SKTypeface.Clone palette)#4265
mattleibow wants to merge 2 commits into
mainfrom
mattleibow-coretext-colr-palette-fix

Conversation

@mattleibow

Copy link
Copy Markdown
Contributor

Summary

SKTypeface.Clone(paletteIndex) and per-entry SKFontArguments.PaletteOverrides were silently ignored on macOS/iOS. The CoreText backend always rendered CPAL palette 0, while Linux (FreeType) and Windows (DirectWrite) honored the selected palette. So COLR/CPAL color-font palette selection worked everywhere except Apple platforms.

Root cause

CoreText exposes no public API to select a COLR/CPAL palette and always renders the font's default palette; the only color input it honors is the CGContext fill color (mapped to the COLR foreground entry). SkTypeface_Mac::onMakeClone forwarded only variation axes and dropped args.getPalette() entirely.

Fix

Native fix lives in the skia submodule — mono/skia#270 (this PR bumps externals/skia to it). At clone/load time it bakes the resolved palette (selected index + per-entry overrides) into palette 0 of an in-memory copy of the font's CPAL table and rebuilds the CTFont from those bytes, using public CoreText API only (App Store safe — a prior private-SPI attempt was reverted upstream, see flutter/flutter#158423).

Tests (prove it works)

New SKColorFontPaletteRenderingTest + a small self-contained COLRv0 test font (tests/Content/fonts/colr-v0-palettes.ttf, 3 palettes × 2 entries, CC0):

  • DifferentPaletteIndexProducesDifferentRendering / AllPalettesProduceDistinctRenderings — rendered pixels are actually different per palette index.
  • PaletteSelectionUsesExpectedColors — each palette paints the expected colors (pal0 red/lime, pal1 blue/yellow, pal2 magenta/cyan).
  • PaletteOverrideChangesEntryColor / NoOpPaletteOverrideMatchesBasePalette — per-entry overrides recolor the targeted entry.
  • OutOfRangePaletteIndexFallsBackToBasePalette / SamePaletteIndexRendersIdentically — semantics match FreeType/CSS.

These COLRv0 tests run and pass on all backends including CoreText.

Known limitation (COLRv1)

CoreText renders COLRv0 (layered solid colors) and honors the baked palette, but does not paint COLRv1 (gradients) at all on current macOS — it falls back to a monochrome outline regardless of palette. ColrV1PaletteSelectionIsUnsupportedOnCoreText documents this: skipped on Apple platforms, asserted to work on FreeType/DirectWrite. Full COLRv1 palette support on Apple would need Skia's own COLR engine (separate effort).

Verification

macOS arm64 (macOS 26.5): 7/8 palette tests pass + 1 skipped (COLRv1 on Mac); typeface/font regression slice clean (SKTypeface 76, SKFont 65, SKFontManager 21 — 0 failures). CI will validate Linux/Windows/Apple.


Depends on mono/skia#270 (merge that first, then re-point this submodule to the merged commit).

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

SKTypeface.Clone(paletteIndex) and per-entry SKFontArguments.PaletteOverrides
were silently ignored on macOS/iOS: the CoreText backend always rendered
CPAL palette 0, while Linux (FreeType) and Windows (DirectWrite) honored
the selected palette.

Bump the skia submodule to the CoreText palette fix (mono/skia#270), which
bakes the resolved palette into palette 0 of an in-memory copy of the font
(public CoreText API only, App Store safe) so the requested colors render.

Add SKColorFontPaletteRenderingTest plus a small self-contained COLRv0 test
font (colr-v0-palettes.ttf, 3 palettes) that proves the rendered colors are
distinct per palette index and that per-entry overrides recolor the targeted
entry. COLRv0 is verified on all backends including CoreText; a COLRv1 test
documents that CoreText does not paint COLRv1 (skipped on Apple platforms,
asserted on FreeType/DirectWrite).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

📦 Try the packages from this PR

Warning

Do not run these scripts without first reviewing the code in this PR.

Step 1 — Download the packages

bash / macOS / Linux:

curl -fsSL https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.sh | bash -s -- 4265

PowerShell / Windows:

iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 4265"

Step 2 — Add the local NuGet source

dotnet nuget add source ~/.skiasharp/hives/pr-4265/packages --name skiasharp-pr-4265
More options
Option Description
--successful-only / -SuccessfulOnly Only use successful builds
--force / -Force Overwrite previously downloaded packages
--list / -List List available artifacts without downloading
--build-id ID / -BuildId ID Download from a specific build

Or download manually from Azure Pipelines — look for the nuget artifact on the build for this PR.

Remove the source when you're done:

dotnet nuget remove source skiasharp-pr-4265

@github-actions

Copy link
Copy Markdown
Contributor

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Site
🔗 View Staging Docs
🔗 View Staging Gallery (Blazor)
🔗 View Staging Gallery (Uno Platform)
🔗 View Staging SkiaFiddle

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

The previous test font was a hand-built 1132-byte COLRv0 font with a
non-conformant OS/2 table (fsSelection=0, OS/2 v3, post 2.0). DirectWrite
crashed (0xC0000005) inside sk_typeface_clone_with_arguments when cloning a
non-default palette of it, hanging the Windows .NET Core test leg.

Rebuild the test font from Skia's resources/fonts/colr.ttf (a font Skia
exercises across every backend including DirectWrite) by extending its CPAL to
three palettes. Render U+2662 (diamond, CPAL entry 0) and U+1F600 (face, CPAL
entry 2) so palette swaps change two large, easy-to-detect regions:
  palette 0: red diamond + yellow face
  palette 1: blue diamond + cyan face
  palette 2: lime diamond + magenta face

Also drop ColrV1PaletteSelectionIsUnsupportedOnCoreText: it asserted DirectWrite
behavior (COLRv1 palette difference) that does not hold in this build and is not
part of the CoreText COLRv0 fix being validated.

Verified locally on macOS arm64 (CoreText): 9/9 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mattleibow mattleibow marked this pull request as draft June 29, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

1 participant