Repository navigation
Test texts without frame or GDI: save, export, finalizers (#412, step 9b) - #479
Merged
Merged
Conversation
First part of step 6 of #412. Text gets its glyphs from GDI today (GraphicsPath.AddString, gdi32 for widths and kerning), which does not exist outside Windows. The new internal class TrueTypeFont reads font files itself: - .ttf files and every font of a .ttc collection; CFF-based .otf and bitmap-only fonts are recognized and report HasTrueTypeOutlines=false - glyph contours from glyf/loca with composite glyphs resolved (offsets, scales, 2x2 transforms, point matching) - cmap formats 0, 4, 6 and 12, including symbol fonts - advance widths (hmtx), metrics (head, hhea, OS/2) and names (name) - kerning pairs of the version 0 kern table, as GDI reads them - GetPathData returns the outline in the layout of a GDI+ GraphicsPath after AddString, so GlyphShapeBuilder.PathDataToPath2D can take it unchanged; the quadratic segments become cubic ones by exact degree elevation Nothing uses it yet: the font service that replaces GDI in Text and FontCache follows in the next pull request. The tests compare names, metrics, glyph indices, advance widths, all contours of 112 characters and the kerning with reference data that fontTools wrote from the same font (Liberation Sans, SIL OFL 1.1, added under tests/CADability.Tests/Files/Fonts together with the script that wrote the reference). They also check that the outline is exactly the quadratic curves, a font collection, and invalid input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: 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
) Step 6, part 2b of #412. The managed reader of #466 skipped OpenType fonts with CFF outlines, which many .otf files have, so a text in such a font fell back to the default face. The new internal CffOutlines reads the "CFF " table: - INDEX and DICT structures, the FontMatrix (which is stored rounded, e.g. 0.00048828 for 1/2048) - the Type 2 charstring interpreter: all path operators including the specialized curves and lines, flex, hflex, hflex1, flex1, local and global subroutines with their bias, widths, hints and hint masks (skipped, they do not change the outline), the arithmetic operators - CID-keyed fonts with FDArray and FDSelect (formats 0 and 3) The cubic curves go into the GDI+ path data unchanged. Not supported: CFF2 (variable OpenType fonts) and the deprecated accent composition of endchar (seac). TrueTypeFont gets HasCffOutlines and HasOutlines, and ManagedFontProvider indexes .otf and .otc files too. Tests: make_cff_test_fonts.py makes two small test fonts from a subset of Inter (SIL OFL 1.1): InterCffTest.otf rewritten to use subroutines, the specialized operators, flex and hint masks, and InterCidTest.otf, the same glyphs as a CID-keyed font with two font dicts. The outlines of both are compared command by command with fontTools. Checked locally against fontTools on 18,672 glyphs of the 47 CFF fonts of the Linux container, 7 of them CID-keyed: all equal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The managed font reader now handles variable OpenType fonts. The named
instances of a variable font become faces of their own, with family names
as GDI makes them ("Bahnschrift Light Condensed", "Segoe UI Variable
Display Semibold"), so these fonts no longer fall back to their default
instance or, for CFF2, disappear from the font list.
- FontVariations parses fvar, avar, the gvar header and HVAR, normalizes
user coordinates and computes tuple scalars like fontTools'
supportScalar (peak and intermediate regions).
- TrueType outlines: gvar deltas with shared and private point numbers,
packed deltas, interpolation of untouched points (IUP), phantom points
and the offsets of composite components.
- CFF2 outlines: 32-bit INDEX counts, the Top DICT outside an INDEX,
FDSelect format 4, vsindex and blend with the ItemVariationStore.
- Advance widths come from HVAR, or from the phantom points without HVAR.
They are not rounded. MVAR is not applied.
The test fonts are subsets of Source Sans 3 VF (OFL-1.1), renamed to
"VarTest Sans" because "Source" is a Reserved Font Name. Their reference
outlines and advance widths were written with fontTools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
GdiFontComparisonTests checks on the fonts installed on Windows that the ManagedFontProvider, the default since the font service, gives the same results as the GdiFontProvider, which is how CADability got its fonts before: - every family GDI+ knows is known to the managed provider; - for a list of common fonts (TrueType, a collection and variable fonts), in every real style: the metrics, the advance widths (within one font unit) and the outlines (Hausdorff distance within two font units), including underline and strikeout; - for all installed families: the metrics, the advance widths and the outlines of a few characters. The tests only compile for net8.0-windows and run in the Windows CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
GDI cuts family names to 31 characters (LF_FACESIZE), e.g. "Bahnschrift SemiBold SemiConden", and texts saved with GDI keep that name. The ManagedFontProvider now resolves such a name to the only family whose name starts with it. The comparison with GDI checks that every family name of GDI resolves to a face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The comparison with GDI on Windows found three differences, which a diagnostic run confirmed against the raw font tables: - Fonts that set USE_TYPO_METRICS (bit 7 of fsSelection) get their cell and line spacing from the typographic metrics of OS/2 in GDI+, not from usWinAscent and usWinDescent (Bahnschrift, Cascadia, Sitka, Cambria Math, Gabriola, Nirmala Text). - GDI+ centers the underline and the strikeout line on the positions of "post" and "OS/2"; they were drawn below them. - When xMin of a glyph is not its left side bearing in "hmtx", Windows, FreeType and fontTools move the outline so that the origin is at 0 (Times New Roman Italic, Symbol). The varied origin of a variable font is taken into account. The Windows test now compares the outlines with the unhinted outlines of GDI (GetGlyphOutline with GGO_UNHINTED): GraphicsPath.AddString returns hinted outlines, which differ from the design by a few font units in strongly hinted fonts and are wrong for Segoe Script, while the managed reader matches the unhinted outlines exactly. The underline and strikeout lines are still compared with GDI+. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The comparison with GDI on Windows found more differences: - Windows names the instances of a variable font from "STAT": the names of the axis values in the axis ordering of "STAT", without the elidable ones. Segoe UI Variable has the instance "Light Display" in "fvar", which Windows calls "Segoe UI Variable Display Light". Without a usable "STAT" table the subfamily name of "fvar" is used as before. - "MVAR" is now applied to the metrics of an instance (typographic and Windows ascent and descent, underline, strikeout). Bahnschrift varies the size of its strikeout line. - A composite glyph with USE_MY_METRICS takes its origin from that component (Times New Roman Bold 'Ü'). The Windows test creates the GDI font with FW_DONTCARE for a regular face, because FW_NORMAL makes GDI embolden a lighter face such as "Cascadia Code ExtraLight". The strikeout values of the variable test fonts at their instances were added to the reference data with fontTools' instancer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
…412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
Step 9 of #412 asks for headless tests in the Linux CI that create, measure and export a text and check that the process survives the finalizers. Creating and measuring was already tested with the font service; these tests add: - saving a text to JSON and reading it back, with the same extent; - exporting a text to DXF and reading it back (string, size, location, and an extent measured with the managed font reader); - running the finalizers after texts have been measured and outlined. 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
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
…nto feature/font-service
FontFace.GetAdvanceWidth, GetKerning and GetGlyphPath took a char, a UTF-16 code unit. A character outside the Basic Multilingual Plane, e.g. most emoji, is a surrogate pair of two chars in a string, so it could never reach a font: the text went through the font char by char. They now take a code point (int). FontFace is new public API of this PR, so this is the time to change it; after a release, every custom font provider would break. - Text and FontCache go through the text as code points (Text.CodePoints): a surrogate pair is one character, a lone surrogate stays the char it is. - The managed face passes the code point on to the cmap, which already supports format 12. - The GDI face measures a character beyond U+FFFF with GetTextExtentPoint32, because GetCharABCWidths only takes UTF-16 chars, and reads the kerning pairs as unsigned values. The new FontCodePointTests check with a recording font provider that "a", an emoji and "b" reach the font as three characters, with their widths, kerning and outlines; they fail when the surrogate pair is not combined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
With StepImport.Parallel, CreateEntity runs on several threads, but it added mapped items to the HashSet mappedItems and, for assemblies, products to the HashSet products without a lock (the other place that adds products locks it). Concurrent Add calls can corrupt a HashSet, and the enumeration of mappedItems after the creation then met a null entry: a NullReferenceException in ImportStep.Read. StepCyclicReferenceTest from #498 imports a file with mapped items 20 times in parallel and showed this now and then on the 2-core CI runner. With the test process limited to 2 cores, it failed 3 of 30 times without this change and 0 of 60 times with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 (cherry picked from commit 85e21f1)
With #468 the texts get their fonts from FontProvider.Current, which is the managed TrueType reader on every platform. To keep the texts of existing Windows Forms applications exactly as before, CADability.Forms now makes its GdiFontProvider the default as soon as one of its main classes is used (CadControl, CadCanvas, CadFrame, PaintToOpenGL, PaintToGDI): their static constructors call GdiFontProvider.MakeDefault. The kernel keeps the managed provider as its own default, so nothing changes for applications without CADability.Forms (the browser, other platforms, renderers of their own). A provider the application sets through FontProvider.Current is kept, whether it is set before or after CADability.Forms is loaded, so an application can still choose the managed provider on Windows. FontProvider tells the two cases apart: the setter marks the provider as the application's choice, the new internal FontProvider.SetDefault replaces only a default. CADability.Forms reaches it through InternalsVisibleTo, so there is no new public API. Refs #412 Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 Co-authored-by: Claude <noreply@anthropic.com>
…xport Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #412, step 9 (verification on non-Windows hosts). This PR is stacked on #472 because it needs the font service of the kernel (#468 and its successors). Its base is
feature/gdi-glyph-comparison; it will be retargeted as the stack is merged.What step 9 asks for
Headless tests in the Linux CI that create, measure and export a
Text, and check that the process survives the finalizers.Creating and measuring a text without a frame and without GDI is already tested since #468 (
FontProviderTests.TextWorksWithoutFrameAndWithoutGdi,FontCacheHasNoFinalizerThatCanCrashTheProcess). This PR adds the export.Tests
HeadlessTextExportTests, using the font Liberation Sans from the test fonts:Project.Export(…, "dxf")and imported again. The test checks the string, the size and the location, and that the imported text gets an extent measured by the managed font reader.GC.Collect,WaitForPendingFinalizersandGC.Collectrun. A finalizer that throws would end the test host and fail the run. That is exactly what the GDIFontCachedid on Linux before Get the fonts of texts from a font service of the kernel instead of GDI #468.STL export is not tested, because
PaintToSTLdoes not triangulate texts (TriangulateTextis alwaysfalse). Texts are left out of STL files on every platform.Local run (Linux):
CADability.Tests: 654 passed, 12 skipped.🤖 Generated with Claude Code
https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
Generated by Claude Code