Variable font support#3703
Conversation
📦 Try the packages from this PRWarning 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 -- 3703PowerShell / Windows: iex "& { $(irm https://raw.githubusercontent.com/mono/SkiaSharp/main/scripts/get-skiasharp-pr.ps1) } 3703"Step 2 — Add the local NuGet source dotnet nuget add source ~/.skiasharp/hives/pr-3703/packages --name skiasharp-pr-3703More options
Or download manually from Azure Pipelines — look for the Remove the source when you're done: dotnet nuget remove source skiasharp-pr-3703 |
There was a problem hiding this comment.
Pull request overview
This PR adds OpenType variable font (font variations) support to the managed SkiaSharp and HarfBuzzSharp APIs, including new querying/cloning APIs on SKTypeface, variation-axis APIs on HarfBuzzSharp.Face, and variation-setting APIs on HarfBuzzSharp.Font, along with new tests and updated generated bindings.
Changes:
- Add SkiaSharp variable-font APIs (
SKTypeface.GetVariationDesignParameters,GetVariationDesignPosition,CloneWithArguments) plus tag helper utilities. - Add HarfBuzzSharp variable-font APIs for axis queries, named instances, and setting/getting variation coordinates; enable generation for
hb-ot-var.h. - Add SkiaSharp and HarfBuzzSharp tests covering variable-font behavior; update the generator’s include-path discovery for Linux.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/SkiaSharpGenerator/BaseTool.cs | Adds Linux system/Clang include discovery for header parsing. |
| tests/Tests/SkiaSharp/SKVariableFontRenderingTest.cs | New rendering-based tests validating variable font rendering differences and clone stability. |
| tests/Tests/SkiaSharp/SKTypefaceTest.cs | Adds SKTypeface-level tests for variation axes/position and cloning. |
| tests/Tests/HarfBuzzSharp/HBFontTest.cs | Adds HarfBuzz Font tests for setting variations, coords, and named instances. |
| tests/Tests/HarfBuzzSharp/HBFaceTest.cs | Adds HarfBuzz Face tests for variation axes, named instances, and presence checks. |
| binding/libHarfBuzzSharp.json | Stops excluding hb-ot-var.h so bindings for var APIs can be generated. |
| binding/SkiaSharp/SkiaApi.generated.cs | Adds P/Invokes for Skia typeface variation APIs and new structs. |
| binding/SkiaSharp/SKTypeface.cs | Exposes the new variable-font APIs on SKTypeface. |
| binding/SkiaSharp/SKFontVariationAxis.cs | Adds tag helpers (SKFontVariationTag) and readable tag-name properties on new structs. |
| binding/HarfBuzzSharp/HarfBuzzApi.generated.cs | Adds P/Invoke surface for hb-ot-var.h. |
| binding/HarfBuzzSharp/Font.cs | Adds variable-font setters/getters on HarfBuzzSharp.Font. |
| binding/HarfBuzzSharp/Face.cs | Adds variable-font axis/named-instance query APIs on HarfBuzzSharp.Face. |
|
/azp run |
|
No pipelines are associated with this pull request. |
|
Hey @ramezgerges, quick question: did you run Since we never got a chance to update the harfbuzz API bindings to the newer HarfBuzz version, I have been just bumping the underlying harfbuzz DEPS version and not re-generating the API. The changes now are so big that the work is a bit harder to do. So many weird types and things. What I usually do is drop back to 2.8.2 and the run the generator. # In externals/skia/DEPS, revert harfbuzz back to the version we last generated from:
# Change: 2b3631a866b3077d9d675caa4ec9010b342b5a7c
# To: 63e15eac4f443fa53565d1e4fb9611cdd7814f28
dotnet cake --target=git-sync-deps
pwsh ./utils/generate.ps1This will show us exactly what needs updating in binding/libHarfBuzzSharp.json so we can make sure the API is usable and not just direct mapping without the human touch. There is a goal at some point to actually expose more, but never got to it. The version of harfbuzz being built is much later (still a bit old now) but not actually 2.8.2 - I think 8.x at least. There is a dream to one day bump harfbuzz and regen the full API and then be amazed. |
|
@mattleibow Yes, I ran the script for harfbuzz and took only the relevant parts. The PR does contain changes to the json files. |
7f97e15 to
1c4fca3
Compare
Native layer (mono/skia submodule):
- C API: sk_typeface_get_variation_design_position,
sk_typeface_get_variation_design_parameters, sk_typeface_clone_with_arguments
- New structs: sk_fontarguments_variation_axis_t,
sk_fontarguments_variation_position_coordinate_t
SkiaSharp managed bindings:
- SKTypeface: GetVariationDesignParameters, GetVariationDesignPosition, Clone
- SKFontVariationTag: Make("wght") / GetName(tag) helper
- TagName property on SKFontVariationAxis and SKFontVariationDesignPositionCoordinate
- Generated P/Invoke bindings and struct type mappings in libSkiaSharp.json
HarfBuzzSharp managed bindings:
- Face: HasVariationData, axis queries, named instance enumeration
- Font: SetVariations, Set/GetVarCoords, SetVarNamedInstance
- P/Invoke bindings for hb-ot-var.h (removed from exclude list)
Tests:
- HBFaceTest, HBFontTest, SKTypefaceTest, SKVariableFontRenderingTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1c4fca3 to
379a101
Compare
- Face: validate instanceIndex >= 0 on named instance methods - Face: explicit (int) cast on uint array lengths - Font: validate instanceIndex >= 0 on SetVarNamedInstance - Tests: dispose Blob in CreateVariableFace/CreateVariableFontPair helpers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
No pipelines are associated with this pull request. |
Interactive demo that showcases the new variable font APIs: - Weight axis slider (100–900) with real-time text rendering - Optical size axis slider (14–32) - Weight spectrum showing all 9 standard weights - Uses Inter Variable font (SIL OFL license, 2 axes: wght, opsz) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
5 similar comments
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
Default parameters break ABI stability. Split into two overloads: - Clone(position) delegates to Clone(position, 0) - Clone(position, collectionIndex) contains the implementation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Incorporates libpng 1.6.58, libexpat 2.7.5, and stream move int32 fix. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
…pport # Conflicts: # binding/SkiaSharp/SkiaApi.generated.cs
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
2 similar comments
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
|
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command. |
) Add variable font C API for typeface variation queries and cloning (#185) Requires: mono/SkiaSharp#3703 Expose SkTypeface's OpenType Font Variations functionality through the SkiaSharp C API, enabling runtime variable font axis querying and typeface cloning with specific design coordinates. New functions: * sk_typeface_get_variation_design_position — query a typeface's current axis coordinate values (e.g. weight, width, optical size) * sk_typeface_get_variation_design_parameters — query axis metadata including tag, min, default, max, and isHidden * sk_typeface_clone_with_arguments — clone a typeface with specific variation coordinates via SkFontArguments New types: * sk_fourbytetag_t — uint32_t typedef matching SkFourByteTag * sk_fontarguments_variation_position_coordinate_t — axis tag + value * sk_fontarguments_variation_axis_t — tag, min, def, max, isHidden The coordinate struct is layout-compatible with Skia's SkFontArguments::VariationPosition::Coordinate (verified by static_assert in sk_structs.cpp), so it uses DEF_MAP for zero-cost reinterpret_cast conversion. The axis struct requires a field-by-field copy because SkFontParameters::Variation::Axis exposes isHidden through a getter rather than a bare field.
Update externals/skia to include the merged variable font C API (mono/skia#185) at the tip of the skiasharp branch. Changes: https://github.com/mono/skia/compare/0afa08b9ac..b6a88e589f Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Distortable.ttf has 3 named instances, so the test should assert count > 0 rather than silently returning. This was flagged in review but the fix was not applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The test had no assertion on the cloned typeface — it passed as long as Clone did not throw. Skia returns a valid typeface for static fonts, so assert not-null to match the contract validated by the other clone tests. Flagged in review but the fix was not applied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Initialize axisInfo = default in TryFindVariationAxis before P/Invoke so callers never observe undefined data when HarfBuzz returns false - Cast uint length to int in VariationCoordsNormalized property for correct array allocation and loop indexing - Remove excessive blank lines in SKTypefaceTest.cs between test sections Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| public int GetNamedInstanceDesignCoordsCount (int instanceIndex) | ||
| { | ||
| if (instanceIndex < 0) | ||
| throw new ArgumentOutOfRangeException (nameof (instanceIndex)); | ||
|
|
||
| // Return value is the total number of design coordinates | ||
| return (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); | ||
| } |
There was a problem hiding this comment.
hb_ot_var_named_instance_get_design_coords takes a coords_length pointer (in/out). Passing null for coords_length (even when coords is null) risks a native null-dereference. For the “count query” call, pass a non-null uint coordsLength pointer and read the updated value (similar to how hb_face_get_table_tags is used elsewhere in this file).
| public float[] GetNamedInstanceDesignCoords (int instanceIndex) | ||
| { | ||
| if (instanceIndex < 0) | ||
| throw new ArgumentOutOfRangeException (nameof (instanceIndex)); | ||
|
|
||
| // Return value is the total number of design coordinates | ||
| var totalCoords = (int)HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, null, null); | ||
| if (totalCoords == 0) | ||
| return Array.Empty<float> (); | ||
|
|
||
| uint coordsLength = (uint)totalCoords; | ||
| var coords = new float[totalCoords]; | ||
| fixed (float* ptr = coords) { | ||
| HarfBuzzApi.hb_ot_var_named_instance_get_design_coords (Handle, (uint)instanceIndex, &coordsLength, ptr); |
There was a problem hiding this comment.
This method also queries hb_ot_var_named_instance_get_design_coords with coords_length = null. Besides the potential native crash, relying on the return value here may be incorrect if HarfBuzz reports the required length via the coords_length out-parameter. Use a non-null coordsLength variable for the size query, then allocate/copy based on that value.
|
|
||
| public static implicit operator SKFourByteTag (uint tag) => new SKFourByteTag (tag); | ||
|
|
||
| public override bool Equals (object obj) => |
There was a problem hiding this comment.
With nullable enabled in this file, Equals(object obj) will produce a nullability-mismatch warning when overriding object.Equals(object?). Either update the signature to Equals(object? obj) (and adjust the pattern matching accordingly) or add #nullable disable at the top to match the rest of the SkiaSharp binding files (e.g., SKColor.cs, SKTypeface.cs).
| public override bool Equals (object obj) => | |
| public override bool Equals (object? obj) => |
|
No pipelines are associated with this pull request. |
Re-runs `pwsh ./utils/generate.ps1 binding/libSkiaSharp.json` against
the current submodule head so the bindings reflect the merged state of:
- the m147 C API surface (sk_pathbuilder_t, R16Unorm, removed
SkGradientShader, m144 SkColorType insertions, m145 SkCodec::Options,
m147 VulkanYcbcrConversionInfo, ...),
- the upstream-on-skiasharp-branch additions
(sk_fontmgr_legacy_create_typeface, variable-font sk_typeface_*
queries, sk_fontarguments_variation_axis_t, sk_fourbytetag_t,
SkManagedStream::move int32_t signature),
- the new XPS doc-with-options entry point.
HarfBuzzApi.generated.cs is intentionally NOT regenerated here.
origin/main's variable-fonts PR (mono#3703) already regenerated it
against the same HarfBuzz 8.3.1 tree as part of that PR; running
the generator a second time on our branch picks up extra HB 7.0+
callback-style APIs (color line, draw funcs, paint funcs) that
have no hand-written DelegateProxies companion in this repo, and
the resulting build fails with CS8795 / CS0246. Re-wiring those
proxies is a separate piece of work — out of scope for the m147
bump.
During the rebase onto origin/main I took --ours on the three
SkiaApi.generated.cs conflicts to keep the rebase moving (those
conflicts were just generator-output collisions and would have
been regenerated anyway). This commit is that regeneration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…3742) Add color font palette support and improve variable font robustness (#3742) Requires: mono/skia#197 Extends the variable font support merged in #3703 with color font palette APIs, Span method robustness fixes, and a pre-existing bug fix. ~~ Color font palette support ~~ SkiaSharp: * SKTypeface.Clone(int paletteIndex) — clone with a built-in palette * SKTypeface.Clone(SKFontArguments) — clone with palette index, per-color overrides, variation position, and collection index * SKFontArguments ref struct gains PaletteIndex and PaletteOverrides * SKFontPaletteOverride struct for individual color entry overrides HarfBuzzSharp: * Face.HasPalettes, PaletteCount — query color font support * Face.GetPaletteColors (array + Span) — retrieve palette entries * Face.GetPaletteFlags, GetPaletteNameId, GetPaletteColorNameId * Face.HasColorLayers, HasColorPng, HasColorSvg C API (mono/skia#197): * sk_fontarguments_palette_override_t struct * sk_typeface_clone_with_arguments extended with palette parameters * DEF_MAP + static_assert for Palette::Override ~~ Span method robustness ~~ Skia's getVariationDesignParameters/Position use all-or-nothing semantics: if the buffer is undersized, they return the total count but write nothing. The C# Span methods now detect this and retry with a pooled buffer (ArrayPool), copying what fits. Empty spans return 0 immediately without native calls or allocations. Font.GetVariationCoordsNormalized(Span) fixed to return the written count instead of the total length when the buffer is undersized. ~~ Bug fix ~~ SKTypeface.ContainsGlyphs(ReadOnlySpan<byte>, SKTextEncoding) had a pre-existing infinite recursion (called itself instead of GetFont().ContainsGlyphs) causing StackOverflowException. ~~ Test coverage ~~ Every Span API tested across empty, undersized, exact, and oversized buffers with exact assertions. Palette APIs tested for colors, flags, name IDs, different palettes, and negative index validation. Added InterVariable.ttf (multi-axis) and test_glyphs-COLRv1.ttf (3 palettes) as test fonts. Gallery sample demonstrates Nabla (COLRv1, 7 palettes, variable axes) with palette picker and weight/optical size sliders. Co-authored-by: Ramez Gerges <ramezragaa@proton.me> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
) Expand API design docs and rename add-api skill to api-add-review (#3749) Context: #3703 Patterns learned while building the variable font API (#3703) were not captured in the developer documentation. Future API work — whether P/Invoke bindings or pure C# libraries in source/ — kept rediscovering the same conventions around Span overloads, struct conversion macros, generator config, and test strategies. documentation/dev/api-design.md now covers: * Span overload triple (array property + count property + Span fill method) * properties vs methods decision rule * type wrapping for uint32_t typedefs (SKFourByteTag pattern) * ref struct parameter bags for complex APIs (SKFontArguments pattern) * test patterns: exact values, Span equivalence, interop round-trip, static font handling, negative index validation * XML doc and file convention guidelines documentation/dev/adding-apis.md now covers: * DEF_MAP macro variants and when to use each * layout-compatible vs manual struct conversion * parameter bag helper functions * static_assert in sk_structs.cpp for ABI verification * libSkiaSharp.json configuration: type aliases, member renames, struct options, function parameter overrides * GPU-specific patterns: GR* prefixes, #if SK_GANESH guards The add-api skill is renamed to api-add-review to reflect its two modes (add new APIs and review existing PRs) and broader applicability to all C# library code, not just P/Invoke bindings. The monolithic SKILL.md is split into a mode dispatcher plus three focused references: add-workflow.md (9-phase), review-workflow.md (structured checklist with auto-fix), and api-design-rules.md (quick reference). All 9 cross-references updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Variable Font Support
Adds OpenType variable font APIs to SkiaSharp and HarfBuzzSharp, enabling runtime font variation axis control, typeface cloning with custom design positions, and full axis/instance querying.
Related skia PR: mono/skia#185
New Types
SKFourByteTagSkFourByteTag. Parse, ToString, implicit uint conversion.SKFontVariationAxisTag,Min,Default,Max,IsHiddenSKFontVariationPositionCoordinateAxis(tag),Value(float)SKFontArgumentsVariationDesignPosition,CollectionIndex— accepts spans, arrays, stackallocNew
SKTypefaceAPIsVariationDesignParametersSKFontVariationAxis[]— all axes the font supportsVariationDesignParameterCountint— axis count for span pre-allocationGetVariationDesignParameters(Span<>)VariationDesignPositionSKFontVariationPositionCoordinate[]— current axis valuesVariationDesignPositionCountintGetVariationDesignPosition(Span<>)Clone(ReadOnlySpan<SKFontVariationPositionCoordinate>)Clone(SKFontArguments)New HarfBuzzSharp
FaceAPIsHasVariationDatafvartableVariationAxisCountVariationAxisInfosOpenTypeVarAxisInfo[]GetVariationAxisInfos(Span<>)TryFindVariationAxis(Tag, out info)NamedInstanceCountGetNamedInstanceDesignCoordsCount(int)GetNamedInstanceDesignCoords(int)GetNamedInstanceDesignCoords(int, Span<float>)GetNamedInstanceSubfamilyNameId(int)GetNamedInstancePostScriptNameId(int)New HarfBuzzSharp
FontAPIsSetVariations(ReadOnlySpan<Variation>)SetVariationCoordsDesign(ReadOnlySpan<float>)SetVariationCoordsNormalized(ReadOnlySpan<int>)VariationCoordsNormalizedint[]— current normalized coordsGetVariationCoordsNormalized(Span<int>)SetVariationNamedInstance(int)Usage — Array-based
Usage — Allocation-free with Span
Console output
Visual comparison
C API changes (companion PR in mono/skia)
sk_fourbytetag_ttypedef matchingSkFourByteTagsk_fontarguments_variation_axis_tandsk_fontarguments_variation_position_coordinate_tstructssk_typeface_get_variation_design_parameters(),sk_typeface_get_variation_design_position(),sk_typeface_clone_with_arguments()DEF_MAPfor layout-compatible struct conversion,static_assertinsk_structs.cppTests
151 tests covering:
SKFourByteTag: Parse, ToString, char constructor, implicit conversion, equality, edge cases (null, empty, short, long strings), known tag hex values, native round-tripGallery sample
Variable Fonts sample using Inter — weight and optical size sliders with weight spectrum comparison strip.