Skip to content

Get the fonts of texts from a font service of the kernel instead of GDI - #468

Open
dsn27 wants to merge 4 commits into
feature/managed-font-providerfrom
feature/font-service
Open

dsn27 wants to merge 4 commits into
feature/managed-font-providerfrom
feature/font-service

Conversation

@dsn27

@dsn27 dsn27 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Step 6 of #412, part 2 of 3.

Stacked on #466. The base branch is feature/managed-font-provider, so the diff shows only this part. Once #466 is merged, change the base to master. Both are also merged into the integration branch integration/issue-412 (#467).

What changes

Text and FontCache no longer use GDI or GDI+. Outlines, advance widths, kerning, metrics and the list of font families all come from the static, settable FontProvider.Current, and no longer from FrameImpl.MainFrame. Texts therefore work without a frame and on every platform.

New public API (minor version)

Type Assembly Purpose
FontProvider kernel abstract: Current (static, settable), GetFamilyNames(), GetFace(family, bold, italic), GetDefaultFace(bold, italic)
FontFace kernel abstract: metrics in font units, GetAdvanceWidth, GetKerning (em units), GetGlyphPath
ManagedFontProvider kernel default provider, reads TrueType files itself (#466)
GdiFontProvider CADability.Forms the previous GDI implementation, opt-in

FontFace.GetGlyphPath returns Bezier path data in the layout of GraphicsPath.AddString. The kernel builds the geometry from it with the existing GlyphShapeBuilder, as before. A provider therefore cannot flatten the outlines, and every provider produces the same exact curves (problem 2 in #412).

The new API uses bool bold, bool italic and not System.Drawing.FontStyle, so step 8 does not have to break it again.

ManagedFontProvider (default on all platforms, as decided for #412)

  • It indexes the system font directories of Windows, Linux and macOS, including fonts installed per user.
  • Indexing reads only the header tables of each file (head, hhea, maxp, OS/2, post, name), so hundreds of fonts are indexed quickly. A font file is loaded completely only when one of its faces is used.
  • AddFontFile and AddFontData add more fonts, e.g. embedded resources.
  • Face selection: the exact style first, then the same weight, then regular, then any face. A missing bold or italic face is not synthesized; GDI does synthesize it.
  • An unknown family falls back to GetDefaultFace: Microsoft Sans Serif, Arial, Liberation Sans, DejaVu Sans, Helvetica, Noto Sans, FreeSans, otherwise the first family.

Problem 1 of #412 is fixed

  • No GDI handle in FontCache: it no longer creates a device context in its constructor or deletes one in its finalizer. Outside Windows the constructor threw and the finalizer then ended the process.
  • Without any font:
    • Painting and the extent of a text leave the text out instead of failing.
    • GetOutline, GetShapes and GetCenterline throw an InvalidOperationException that says what to do.

Other changes

  • ShowPropertyFont and DimensionStyle list the families of the provider instead of System.Drawing.FontFamily.Families.
  • TrueTypeFont:
    • reads the underline and strikeout metrics (post, OS/2) and adds both as rectangles over the advance width, as GDI+ does;
    • can read only the header tables of a file (ReadHeaders).

Behaviour changes for the release notes

  • On Windows, texts now use the managed reader by default. The glyphs come from the same font files and should match GDI within float precision. Part 3 adds the Windows test that compares them.

  • What the managed reader leaves out:

    • OpenType fonts with CFF outlines (some .otf files) cannot be read. A text in such a font falls back to the default face. Read the CFF outlines of OpenType fonts in the managed font reader #470 removes this limitation.
    • Variable fonts: only the default instance is known (added after the first Windows CI run). GDI lists the named instances of a variable font as families of their own, e.g. "Bahnschrift Light Condensed" or "Segoe UI Variable Text" on Windows 11. The managed reader reads only the default outlines (glyf without gvar) and knows only the family of the default instance, e.g. "Bahnschrift". A text in such a named instance falls back to the default face. Variable OpenType fonts with CFF2 outlines are left out completely. The Windows CI run of the integration branch showed this: the glyph tests for these instances were skipped there.
    • Bold and italic faces a family does not have are not synthesized.

    Applications that need any of these can set FontProvider.Current = new GdiFontProvider(). Support for variable fonts (named instances, gvar and CFF2) is a possible follow-up.

  • CADability.Gdi (public) is no longer used by the kernel for fonts. It stays until 2.0.

Tests

  • Test fonts: Both test projects register Liberation Sans (bundled under tests/CADability.Tests/Files/Fonts, OFL) in an [AssemblyInitialize], in addition to the system fonts. Texts therefore have a font even on a build server without any fonts installed.
  • Text tests now run on Linux: The tests that were inconclusive there ("text needs GDI") now run: 6 in ProjectTest and 5 in ImportTests. The guards are removed.
  • New FontProviderTests:
    • text without a frame;
    • its width matches the advance widths and kerning;
    • an unknown font uses the default face;
    • the exception without fonts, while painting and extent survive;
    • underline and strikeout;
    • style fallback and case-insensitive family names;
    • header-only reading;
    • no finalizer that can crash the process.
  • FontCacheGlyphTests: no longer limited to Windows. They run for every installed font, plus a new DataRow for Liberation Sans. The survey over all fonts (CADABILITY_FONT_SURVEY=1) found 0 fonts with glyphs without triangles among the 47 TrueType fonts in the Linux container.

Local run on Linux:

  • CADability.Tests: 633 passed, 12 skipped (Windows fonts that are not installed), 0 failed.
  • CADability.ImportTests: 88 passed, 0 skipped (5 before), 0 failed.
  • The same, again all green, with /usr/share/fonts hidden, which simulates a runner without fonts.
  • The kernel and CADability.Forms build.

On Windows (the Windows CI of #469, run on the integration branch): the reference-bitmap tests in ProjectTest, which now render text from the managed reader, pass. So do the glyph tests for Arial, Calibri, Segoe UI and Bahnschrift.

No change to the csproj version.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

…DI (#412)

Second part of step 6 of #412. Text and FontCache no longer use GDI or
GDI+: the glyph outlines, advance widths, kerning, metrics and the list
of font families come from the static, settable FontProvider.Current,
not from FrameImpl.MainFrame, so texts work without a frame and on
every platform.

- FontProvider and FontFace (public, new) describe what the kernel
  needs. A face returns its glyphs as Bezier path data in the layout of
  GraphicsPath.AddString, which the kernel turns into geometry with
  GlyphShapeBuilder as before, so no provider can flatten the outlines.
- ManagedFontProvider (public, new) is the default on all platforms,
  as decided for #412. It indexes the TrueType fonts of the system font
  directories by reading only their header tables, and loads a font
  file when a face is used. Files and font data can be added.
- FontCache no longer creates a GDI device context in its constructor
  or deletes it in a finalizer, which ended the process outside Windows.
- Without any font, painting and the extent of a text leave it out;
  GetOutline and similar explicit requests throw an
  InvalidOperationException that says what to do.
- ShowPropertyFont and DimensionStyle list the families of the provider.
- CADability.Forms.GdiFontProvider (public, new) is the old GDI
  implementation for applications that want it.

TrueTypeFont reads the underline and strikeout metrics, adds both as
rectangles like GDI+ does, and can read only the header tables of a
font file.

Tests: both test projects register Liberation Sans in an assembly
initializer, so texts have a font even on a machine without fonts. The
text tests that were inconclusive on Linux ("text needs GDI") now run
there; new FontProviderTests cover text without a frame, the default
face for unknown fonts, the exception without fonts, underline and
strikeout, style fallback, header-only reading and the finalizer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
Resolves the conflict with #497 in FontCache: the managed font calls of
this branch now run under the same lock (this) that #497 introduced, and
Clear disposes the glyph display lists and empties all caches, including
the faces. FontProvider.Current, which calls Clear, documents that the
display lists of the views need to be rebuilt when it is set after texts
were painted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

dsn27 commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator Author

Merged master into this branch. #497 conflicted with it in FontCache (Text.cs), because #497 added locking and display list disposal to the GDI version that this PR replaces. The resolution:

The stacked PRs #470, #471, #472 and #479 contain the merge as well; none of them had further conflicts. CADability.Tests on Linux: 655 passed here, 676 on #479.


Generated by Claude Code

ClearingTheFontCacheDisposesTheGlyphLists reflected on the 'kerning' field
of the GDI FontCache, which this branch replaces by the 'faces' of
FontProvider.Current, so it failed with a NullReferenceException on
Windows. It now checks that Clear empties the faces, and it runs on every
platform, because the font cache no longer needs GDI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

dsn27 commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator Author

One more adaptation after the merge with #497.

RendererRecoveryTests.ClearingTheFontCacheDisposesTheGlyphLists reflects on the private kerning field of the GDI FontCache. This PR replaces that field with the faces of FontProvider.Current. On Linux the test was skipped as inconclusive ("needs GDI"), but on Windows it failed with a NullReferenceException; the Windows job of the integration branch #467 showed this.

The test now checks that Clear disposes the glyph list and empties cache, centerLineCache, fontCharacteristicsCache and faces. It also runs on every platform now, because the font cache no longer needs GDI.

CADability.Tests on Linux: 656 passed, 0 failed.


Generated by Claude Code

dsn27 commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

The Linux job on 830f7574 failed in CADability.ImportTests: StepCyclicReferenceTest.import_step_with_representations_that_map_each_other threw a NullReferenceException in ImportStep.Read (ImportStep.cs:1683, the loop over mapping_source.mapped_representation.items). All 659 CADability.Tests passed in the same run.

This failure is not caused by this PR. The test comes from #498, which is now on master, and this PR does not touch ImportStep. The test deliberately runs 20 parallel imports of a STEP file with mutually mapping representations, to provoke a race in the cycle cutting of #498. Locally, on the same commit, it passed 40 times in a row, which is 800 parallel imports. The race therefore seems to show only now and then, on the 2-core runner.

I am re-running the failed job once. If it fails again, the race in the parallel STEP import of #498 needs a fix on master; no such fix exists yet.


Generated by Claude Code

This branch has not been deployed

No deployments
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.

2 participants