Repository navigation
Fix stack overflows in face traversal and STEP reference cycles, set the version to 1.7.0 (#486, #487) - #498
Merged
Merged
Conversation
Make3D.extractConnectedFaces, BRepOperation.extractConnectedFaces and BRepSelfIntersection.collectFaces walked the faces of a shell with a recursive depth first search that nested once per connected face. A DXF mesh of about 7000 triangles (sewing) or a boolean operation on a shell of some ten thousand faces overflowed the stack, which terminates the process. The three traversals use an explicit stack now and visit the faces in the same order as the recursion, so the shells come out unchanged. The new tests sew a grid of 3200 triangles and subtract a box from a prism with 3000 sides on a thread with a 256 KB stack; both crash the test host with a stack overflow without the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cNtmJQqQvHArBc17v66Xu
…487) ImportStep.CreateEntity creates referenced entities recursively and only knew an entity as created once it was finished. A reference cycle, e.g. a MAPPED_ITEM whose REPRESENTATION_MAP maps the representation that contains the mapped item, therefore re-entered CreateEntity for the same entity (the monitor is re-entrant) until the stack overflowed. - Each item records the thread that is creating it. When that thread reaches the item again, the reference closes a cycle: it is created as null and reported as "cyclic reference" in the import problems. - With StepImport.Parallel two threads can enter a cycle from different ends and would then wait for each other's lock forever. A thread that has to wait for an item now follows the chain of threads waiting for each other; when it leads back to an item it is creating itself, it cuts the cycle in the same way instead of waiting. - The DEBUG-only definition stack is no longer left unbalanced by the early return of an entity that another thread has created meanwhile. The new tests import issue101.stp with a representation that maps itself and with two representations that map each other, sequentially and in parallel. Without the change the first crashes the test host with a stack overflow; with only the same-thread check the parallel import of the second does not finish. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cNtmJQqQvHArBc17v66Xu
A minor release: 1.7.0 adds public API (IJsonProgress, DXF.IDxfProgress and the progress overloads, Text.ClearFontCache, ProjectedModel.InvalidateDisplayLists, the PaintTo3DOutOfMemory constructors). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012cNtmJQqQvHArBc17v66Xu
This was referenced Oct 19, 2026
dsn27
pushed a commit
that referenced
this pull request
Oct 19, 2026
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)
dsn27
pushed a commit
that referenced
this pull request
Oct 19, 2026
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)
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
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. Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
* Add a managed TrueType reader for text on every platform (#412) 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 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
… 7a) (#473) * Keep the images of pictures and icons as image file bytes (#412) Picture and Icon kept a System.Drawing.Bitmap and wrote it into project files as a serialized GDI+ object, so a project with a picture could not be opened without GDI+. Now they keep the bytes of the image file (PNG, JPEG, GIF or BMP) and write those, as Base64, into project files. - The kernel does not decode the image. The size in pixels comes from the file header (EncodedImage). On Windows GDI+ decodes the bytes when the picture is painted; without GDI+ a picture is painted as its outline. - New API: Picture.ImageData, PixelWidth, PixelHeight, Set(byte[], ...), SetImageDataNoUndo; Icon.ImageData, PixelWidth, PixelHeight, Set(byte[], ...). - Obsolete in 1.x, removed in 2.0: Picture.Bitmap, Set(Bitmap, ...), SetBitmapNoUndo and Clip (GDI+ only), Icon.Set(Bitmap, ...). - Files of 1.x keep opening on every platform: the serialized GDI+ bitmap is read as its "Data" bytes (LegacyImageData) by the JSON reader and by a binder for binary files. If the image of an old file cannot be read, the picture is loaded from its Path, with a warning. - Image files are read with File.ReadAllBytes, so no stream is left open. pictures-1x.cdb and pictures-1x.cdb.json were written by the current master on Windows, with GDI+ bitmaps, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test files with pictures written on .NET Framework 4.8 (#412) Most project files of CADability 1.x come from .NET Framework, where GDI+ serializes a bitmap in its own format (the JPEG picture stays a JPEG), while System.Drawing.Common on .NET 8 serializes every bitmap as PNG. pictures-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Use a relative path in the path fallback test (#412) The test failed on Windows: JsonSerialize does not escape backslashes when it writes a string, but reads "\r" and "\n" as control characters, so the absolute temp path C:\Users\runneradmin\... did not survive the round trip. That is a bug of JsonSerialize of its own; the test now uses a relative path. The crefs to the overloaded Set methods are made unambiguous (CS0419). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep Picture.Clip without [Obsolete] (#412) Clip has no replacement, and with step 8 of #412 deferred it is not going away: marking it obsolete would only cause warnings, and break builds that treat warnings as errors. The messages of the members that keep [Obsolete] no longer announce a removal in 2.0; they say that a GDI+ Bitmap only works on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
…DI (#468) * Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
* Keep the images of pictures and icons as image file bytes (#412) Picture and Icon kept a System.Drawing.Bitmap and wrote it into project files as a serialized GDI+ object, so a project with a picture could not be opened without GDI+. Now they keep the bytes of the image file (PNG, JPEG, GIF or BMP) and write those, as Base64, into project files. - The kernel does not decode the image. The size in pixels comes from the file header (EncodedImage). On Windows GDI+ decodes the bytes when the picture is painted; without GDI+ a picture is painted as its outline. - New API: Picture.ImageData, PixelWidth, PixelHeight, Set(byte[], ...), SetImageDataNoUndo; Icon.ImageData, PixelWidth, PixelHeight, Set(byte[], ...). - Obsolete in 1.x, removed in 2.0: Picture.Bitmap, Set(Bitmap, ...), SetBitmapNoUndo and Clip (GDI+ only), Icon.Set(Bitmap, ...). - Files of 1.x keep opening on every platform: the serialized GDI+ bitmap is read as its "Data" bytes (LegacyImageData) by the JSON reader and by a binder for binary files. If the image of an old file cannot be read, the picture is loaded from its Path, with a warning. - Image files are read with File.ReadAllBytes, so no stream is left open. pictures-1x.cdb and pictures-1x.cdb.json were written by the current master on Windows, with GDI+ bitmaps, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test files with pictures written on .NET Framework 4.8 (#412) Most project files of CADability 1.x come from .NET Framework, where GDI+ serializes a bitmap in its own format (the JPEG picture stays a JPEG), while System.Drawing.Common on .NET 8 serializes every bitmap as PNG. pictures-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Save page settings as a neutral PageSetup (#412) Project files contained the GDI+ PageSettings of the project and of the layouts. These cannot be serialized on .NET Core: on .NET 8, a project with page settings could not be saved at all (JSON and binary throw), and without GDI+ the page settings of an old file were dropped. - New public PageSetup: paper name, kind and size, orientation, margins (in mm) and printer name. Project.DefaultPageSetup and Layout.PageSetup are saved instead of the PageSettings. - Files of 1.x keep their page settings on every platform: the System.Drawing.Printing objects are read as LegacyPrintingData by the JSON reader and by the binder for binary files (the JSON format has the public properties, the binary format of .NET Framework the private fields, TriStates and enums), and a PageSetup is made from them. - On Windows the page setup is applied to the print document: the printer, if it is installed, the printer's paper of the same kind and size (or a custom one), the orientation and the margins. The page setup and printer dialogs of a layout view now store the result in the layout. - Obsolete in 1.x, moved into CADability.Forms in 2.0: PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage, IUIService.ShowPageSetupDlg and ShowPrintDlg, Layout.pageSettings. pagesettings-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Use a relative path in the path fallback test (#412) The test failed on Windows: JsonSerialize does not escape backslashes when it writes a string, but reads "\r" and "\n" as control characters, so the absolute temp path C:\Users\runneradmin\... did not survive the round trip. That is a bug of JsonSerialize of its own; the test now uses a relative path. The crefs to the overloaded Set methods are made unambiguous (CS0419). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep Picture.Clip without [Obsolete] (#412) Clip has no replacement, and with step 8 of #412 deferred it is not going away: marking it obsolete would only cause warnings, and break builds that treat warnings as errors. The messages of the members that keep [Obsolete] no longer announce a removal in 2.0; they say that a GDI+ Bitmap only works on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the printing members without [Obsolete] (#412) PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage and the printing dialogs of IUIService have no replacement, and with step 8 of #412 deferred they are not going away: marking them obsolete would only cause warnings, and break builds that treat warnings as errors. Layout.pageSettings keeps [Obsolete], because PageSetup replaces it; its message no longer announces a removal in 2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
) * Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
… step 9c) (#480) * Keep the images of pictures and icons as image file bytes (#412) Picture and Icon kept a System.Drawing.Bitmap and wrote it into project files as a serialized GDI+ object, so a project with a picture could not be opened without GDI+. Now they keep the bytes of the image file (PNG, JPEG, GIF or BMP) and write those, as Base64, into project files. - The kernel does not decode the image. The size in pixels comes from the file header (EncodedImage). On Windows GDI+ decodes the bytes when the picture is painted; without GDI+ a picture is painted as its outline. - New API: Picture.ImageData, PixelWidth, PixelHeight, Set(byte[], ...), SetImageDataNoUndo; Icon.ImageData, PixelWidth, PixelHeight, Set(byte[], ...). - Obsolete in 1.x, removed in 2.0: Picture.Bitmap, Set(Bitmap, ...), SetBitmapNoUndo and Clip (GDI+ only), Icon.Set(Bitmap, ...). - Files of 1.x keep opening on every platform: the serialized GDI+ bitmap is read as its "Data" bytes (LegacyImageData) by the JSON reader and by a binder for binary files. If the image of an old file cannot be read, the picture is loaded from its Path, with a warning. - Image files are read with File.ReadAllBytes, so no stream is left open. pictures-1x.cdb and pictures-1x.cdb.json were written by the current master on Windows, with GDI+ bitmaps, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test files with pictures written on .NET Framework 4.8 (#412) Most project files of CADability 1.x come from .NET Framework, where GDI+ serializes a bitmap in its own format (the JPEG picture stays a JPEG), while System.Drawing.Common on .NET 8 serializes every bitmap as PNG. pictures-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Save page settings as a neutral PageSetup (#412) Project files contained the GDI+ PageSettings of the project and of the layouts. These cannot be serialized on .NET Core: on .NET 8, a project with page settings could not be saved at all (JSON and binary throw), and without GDI+ the page settings of an old file were dropped. - New public PageSetup: paper name, kind and size, orientation, margins (in mm) and printer name. Project.DefaultPageSetup and Layout.PageSetup are saved instead of the PageSettings. - Files of 1.x keep their page settings on every platform: the System.Drawing.Printing objects are read as LegacyPrintingData by the JSON reader and by the binder for binary files (the JSON format has the public properties, the binary format of .NET Framework the private fields, TriStates and enums), and a PageSetup is made from them. - On Windows the page setup is applied to the print document: the printer, if it is installed, the printer's paper of the same kind and size (or a custom one), the orientation and the margins. The page setup and printer dialogs of a layout view now store the result in the layout. - Obsolete in 1.x, moved into CADability.Forms in 2.0: PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage, IUIService.ShowPageSetupDlg and ShowPrintDlg, Layout.pageSettings. pagesettings-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Use a relative path in the path fallback test (#412) The test failed on Windows: JsonSerialize does not escape backslashes when it writes a string, but reads "\r" and "\n" as control characters, so the absolute temp path C:\Users\runneradmin\... did not survive the round trip. That is a bug of JsonSerialize of its own; the test now uses a relative path. The crefs to the overloaded Set methods are made unambiguous (CS0419). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test pictures without frame or GDI: measure, export, finalizers (#412) Step 9 of #412 asks for headless tests in the Linux CI that create, measure and export a picture and check that the process survives the finalizers. #473 already tests the image bytes, the JSON round trip and the old files; these tests add: - measuring a picture (pixel size, extent, aspect ratio); - exporting a project with a picture to DXF and STL, which have no pictures: the other objects are written and the picture does not stop the export; - writing and reading a project with a picture in the binary format, which used to contain the GDI+ bitmap (needs the BinaryFormatter, so it runs on .NET 8); - running the finalizers after pictures have been painted and cloned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep Picture.Clip without [Obsolete] (#412) Clip has no replacement, and with step 8 of #412 deferred it is not going away: marking it obsolete would only cause warnings, and break builds that treat warnings as errors. The messages of the members that keep [Obsolete] no longer announce a removal in 2.0; they say that a GDI+ Bitmap only works on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the printing members without [Obsolete] (#412) PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage and the printing dialogs of IUIService have no replacement, and with step 8 of #412 deferred they are not going away: marking them obsolete would only cause warnings, and break builds that treat warnings as errors. Layout.pageSettings keeps [Obsolete], because PageSetup replaces it; its message no longer announces a removal in 2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
* Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Read variable fonts: gvar, CFF2, avar and HVAR (#412) 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 * Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
…rences (#412, step 6) (#472) * Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Read variable fonts: gvar, CFF2, avar and HVAR (#412) 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 * Compare the managed font reader with GDI on Windows (#412) 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 * Find a family by the name GDI cuts to 31 characters (#412) 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 * Lay out glyphs, metrics and decorations like GDI (#412) 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 * Name and measure variable instances like Windows (#412) 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 * Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) * Make the GDI font provider the default in CADability.Forms (#503) 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
* Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Read variable fonts: gvar, CFF2, avar and HVAR (#412) 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 * Compare the managed font reader with GDI on Windows (#412) 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 * Find a family by the name GDI cuts to 31 characters (#412) 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 * Lay out glyphs, metrics and decorations like GDI (#412) 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 * Name and measure variable instances like Windows (#412) 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 * Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) * Synthesize a missing bold or italic face like GDI+ (#412) GDI+ reports every style of every family as available and synthesizes the missing ones, so the texts of CADability before the font service showed a bold or italic text in a family without that face (e.g. Microsoft Sans Serif, Lucida Console, Impact) as synthetic bold or italic. The managed provider took the closest face instead, so such a text would have lost its style on Windows. ManagedFontProvider now wraps the closest face in a SyntheticFontFace: - Italic shears the glyphs by 0x5700/0x10000 (18.77 degrees) around the baseline; the advance widths stay. GDI and GDI+ do exactly this. - Bold moves the outline outwards by half the strength on every side (FT_Outline_EmboldenXY of FreeType) and then by half the strength to the right and up: the left side and the baseline stay, every edge facing right moves right by ceiling(em / 50) font units and every edge facing up moves up by one unit less. A face with a weight of 600 or more is not made wider. The advance widths stay, as with the GDI widths CADability used. The rules were measured with GDI+ on the Windows runner for all installed families without a bold face. TrueTypeFont reads usWeightClass (or the wght axis of an instance) for the weight rule. Tests: the emboldened outlines of Lucida Console and Microsoft Sans Serif 'I' equal what GDI+ returned on Windows to 0.01 font units; holes get smaller; the strength for 256 to 4096 units per em; italic and bold of the regular-only Liberation Sans. SyntheticStyleGdiTests compares the synthetic styles with GDI+ on Windows for several families. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Make the quadratic TrueType outline wider for synthetic bold The first Windows comparison showed the straight edges equal to GDI+, but curves off by a few font units ('o') and some glyphs by up to 35 ('8', serifs of Sylfaen and Gabriola). GDI+ makes the glyph wider on the outline of the font, before its quadratic segments become cubic ones: the points it moves and the limit by the shorter edge depend on that point set. The emboldening now works on the quadratic TrueType contours in font units; a CFF outline is made wider after it was drawn, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the synthetic bold shift within half the strength per axis like GDI+ Measured point by point against GDI+ on the Windows runner: GDI+ does not limit the shift at an inner corner by the length of the shorter edge, as FT_Outline_EmboldenXY does, so the short edges of the rounded serif corners of Sylfaen move by the whole strength. Instead it keeps the shift of every point within half the strength on each axis, so that the sharp corners of the '8' of Microsoft Sans Serif do not shoot out by up to 24 font units. The comparison test now matches the points of the managed and the GDI+ outline by position, because GDI+ starts the contours of MS Gothic at another point. Refs #412 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Compare Sylfaen and Gabriola with GDI+ within 2% of the em height GDI+ moves single points of these two families differently from the other five, without a rule that could be found (e.g. a point in the middle of a straight slanted edge of the Sylfaen '8' only moves up). The differences are up to 1.6% and 0.85% of the em height; the other families keep the tolerance of 2 font units. Refs #412 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Make the GDI font provider the default in CADability.Forms (#503) 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
… 9b) (#479) * Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Read variable fonts: gvar, CFF2, avar and HVAR (#412) 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 * Compare the managed font reader with GDI on Windows (#412) 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 * Find a family by the name GDI cuts to 31 characters (#412) 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 * Lay out glyphs, metrics and decorations like GDI (#412) 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 * Name and measure variable instances like Windows (#412) 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 * Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test texts without frame or GDI: save, export, finalizers (#412) 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 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) * Make the GDI font provider the default in CADability.Forms (#503) 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> --------- Co-authored-by: Claude <noreply@anthropic.com>
dsn27
added a commit
that referenced
this pull request
Oct 19, 2026
* Add a managed TrueType reader for text on every platform (#412) 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 * Get the fonts of texts from a font service of the kernel instead of GDI (#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 * Add a Windows CI workflow that builds and tests with GDI+ and WinForms (#412) The Linux CI compiles the tests that need GDI+, WinForms or OpenGL out (they are guarded by WINDOWS, which only the net8.0-windows target of CADability.Tests defines). The new workflow builds the kernel and CADability.Forms on windows-latest and runs both test projects there, so these tests run on every pull request as well: the GDI+ conversions of CADability.Forms, the rendering against reference bitmaps and, next, the comparison of the GDI and the managed glyph outlines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Make the DXF dimension tests independent of the line endings of the checkout Four tests of DxfDimensionTest edit their verbatim-string fixtures with Replace("...\n..."). With "* text=auto" Windows checks the file out with CRLF, the search text is not found, and the tests fail on their own "fixture should have been changed" assertion. The new Windows CI showed it; Linux checkouts have LF and never noticed. The fixtures are now normalized to LF before they are edited, as the file already does for the DXF written in another test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Read the CFF outlines of OpenType fonts in the managed font reader (#412) 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 * Read variable fonts: gvar, CFF2, avar and HVAR (#412) 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 * Compare the managed font reader with GDI on Windows (#412) 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 * Find a family by the name GDI cuts to 31 characters (#412) 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 * Lay out glyphs, metrics and decorations like GDI (#412) 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 * Name and measure variable instances like Windows (#412) 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 * Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#412) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the images of pictures and icons as image file bytes (#412) Picture and Icon kept a System.Drawing.Bitmap and wrote it into project files as a serialized GDI+ object, so a project with a picture could not be opened without GDI+. Now they keep the bytes of the image file (PNG, JPEG, GIF or BMP) and write those, as Base64, into project files. - The kernel does not decode the image. The size in pixels comes from the file header (EncodedImage). On Windows GDI+ decodes the bytes when the picture is painted; without GDI+ a picture is painted as its outline. - New API: Picture.ImageData, PixelWidth, PixelHeight, Set(byte[], ...), SetImageDataNoUndo; Icon.ImageData, PixelWidth, PixelHeight, Set(byte[], ...). - Obsolete in 1.x, removed in 2.0: Picture.Bitmap, Set(Bitmap, ...), SetBitmapNoUndo and Clip (GDI+ only), Icon.Set(Bitmap, ...). - Files of 1.x keep opening on every platform: the serialized GDI+ bitmap is read as its "Data" bytes (LegacyImageData) by the JSON reader and by a binder for binary files. If the image of an old file cannot be read, the picture is loaded from its Path, with a warning. - Image files are read with File.ReadAllBytes, so no stream is left open. pictures-1x.cdb and pictures-1x.cdb.json were written by the current master on Windows, with GDI+ bitmaps, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test files with pictures written on .NET Framework 4.8 (#412) Most project files of CADability 1.x come from .NET Framework, where GDI+ serializes a bitmap in its own format (the JPEG picture stays a JPEG), while System.Drawing.Common on .NET 8 serializes every bitmap as PNG. pictures-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Save page settings as a neutral PageSetup (#412) Project files contained the GDI+ PageSettings of the project and of the layouts. These cannot be serialized on .NET Core: on .NET 8, a project with page settings could not be saved at all (JSON and binary throw), and without GDI+ the page settings of an old file were dropped. - New public PageSetup: paper name, kind and size, orientation, margins (in mm) and printer name. Project.DefaultPageSetup and Layout.PageSetup are saved instead of the PageSettings. - Files of 1.x keep their page settings on every platform: the System.Drawing.Printing objects are read as LegacyPrintingData by the JSON reader and by the binder for binary files (the JSON format has the public properties, the binary format of .NET Framework the private fields, TriStates and enums), and a PageSetup is made from them. - On Windows the page setup is applied to the print document: the printer, if it is installed, the printer's paper of the same kind and size (or a custom one), the orientation and the margins. The page setup and printer dialogs of a layout view now store the result in the layout. - Obsolete in 1.x, moved into CADability.Forms in 2.0: PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage, IUIService.ShowPageSetupDlg and ShowPrintDlg, Layout.pageSettings. pagesettings-1x-net48.cdb and .cdb.json were written by the current master on .NET Framework 4.8, for the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Use a relative path in the path fallback test (#412) The test failed on Windows: JsonSerialize does not escape backslashes when it writes a string, but reads "\r" and "\n" as control characters, so the absolute temp path C:\Users\runneradmin\... did not survive the round trip. That is a bug of JsonSerialize of its own; the test now uses a relative path. The crefs to the overloaded Set methods are made unambiguous (CS0419). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Import all CSS named colors from SVG (#412) ParseSvgColor relied on ColorTranslator.FromHtml for color names, which only knows the colors of .NET: the six "grey" spellings of CSS (grey, darkgrey, dimgrey, slategrey, darkslategrey, lightslategrey) became transparent black, so their paths were not filled, and rebeccapurple is only known since .NET 8. The SVG import now has its own table of the 148 named colors of CSS (Color Module Level 4). Colors that .NET knows are returned as named colors (Color.Navy), as before; the others as their RGB value. Other names still go through ColorTranslator. The tests check all 148 names in three spellings against the values of the CSS specification and import an SVG file with a square of every color. This is the SVG acceptance criterion of #412 (step 9). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Test texts without frame or GDI: save, export, finalizers (#412) 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 * Test pictures without frame or GDI: measure, export, finalizers (#412) Step 9 of #412 asks for headless tests in the Linux CI that create, measure and export a picture and check that the process survives the finalizers. #473 already tests the image bytes, the JSON round trip and the old files; these tests add: - measuring a picture (pixel size, extent, aspect ratio); - exporting a project with a picture to DXF and STL, which have no pictures: the other objects are written and the picture does not stop the export; - writing and reading a project with a picture in the binary format, which used to contain the GDI+ bitmap (needs the BinaryFormatter, so it runs on .NET 8); - running the finalizers after pictures have been painted and cloned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Sample BSpline2D.AddToGraphicsPath on 0..1 (#412) AddToGraphicsPath passed knot parameters (startParam..endParam) to PointAt, which expects 0..1. A spline whose knots do not run from 0 to 1 was therefore drawn through wrong points; splines with knots from 0 to 1 are unaffected. Taken from the closed #465. The test needs GDI+ and only runs on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Run the kernel as WebAssembly in the Linux CI (#412) tests/CADability.WasmSmoke builds CADability.dll into a .NET WebAssembly program and runs it under Node.js, the runtime a browser front end uses. Without a frame and without GDI+ it registers a font from bytes, builds a solid with a boolean operation, measures a text and a picture, paints the model through an IPaintTo3D of its own (as a browser renderer would), saves and reads JSON, opens project files of 1.x with pictures and page settings, imports STEP, exports DXF and STL, and runs the finalizers. The renderer implements IPaintTo3D as it is, with Bitmap and FontStyle in its signatures: naming GDI+ types is fine in the browser, only creating GDI+ objects fails. This shows that the browser does not need the breaking step 8 of #412. On master, 7 of the 13 checks fail with PlatformNotSupportedException from System.Drawing.Common; with the open #412 pull requests all pass. The project needs no workload and is not part of CADability.sln. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep Picture.Clip without [Obsolete] (#412) Clip has no replacement, and with step 8 of #412 deferred it is not going away: marking it obsolete would only cause warnings, and break builds that treat warnings as errors. The messages of the members that keep [Obsolete] no longer announce a removal in 2.0; they say that a GDI+ Bitmap only works on Windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the printing members without [Obsolete] (#412) PrintToGDI, LayoutView.Print, GDI2DView.PrintSinglePage and the printing dialogs of IUIService have no replacement, and with step 8 of #412 deferred they are not going away: marking them obsolete would only cause warnings, and break builds that treat warnings as errors. Layout.pageSettings keeps [Obsolete], because PageSetup replaces it; its message no longer announces a removal in 2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Disambiguate PaintEventArgs in RendererRecoveryTests for the Windows build The test from #497 uses PaintEventArgs with 'using CADability.Substitutes'. The Windows build of the tests sets UseWindowsForms, which brings System.Windows.Forms.PaintEventArgs into scope as well, so the Windows job failed with CS0104. An alias picks the substitute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Adapt the font cache test of #497 to the font service (#412) 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 * Pass Unicode code points instead of chars to FontFace (#412) 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 * Lock the shared sets of the parallel STEP import 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) * Lock the shared sets of the parallel STEP import 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) * Synthesize a missing bold or italic face like GDI+ (#412) GDI+ reports every style of every family as available and synthesizes the missing ones, so the texts of CADability before the font service showed a bold or italic text in a family without that face (e.g. Microsoft Sans Serif, Lucida Console, Impact) as synthetic bold or italic. The managed provider took the closest face instead, so such a text would have lost its style on Windows. ManagedFontProvider now wraps the closest face in a SyntheticFontFace: - Italic shears the glyphs by 0x5700/0x10000 (18.77 degrees) around the baseline; the advance widths stay. GDI and GDI+ do exactly this. - Bold moves the outline outwards by half the strength on every side (FT_Outline_EmboldenXY of FreeType) and then by half the strength to the right and up: the left side and the baseline stay, every edge facing right moves right by ceiling(em / 50) font units and every edge facing up moves up by one unit less. A face with a weight of 600 or more is not made wider. The advance widths stay, as with the GDI widths CADability used. The rules were measured with GDI+ on the Windows runner for all installed families without a bold face. TrueTypeFont reads usWeightClass (or the wght axis of an instance) for the weight rule. Tests: the emboldened outlines of Lucida Console and Microsoft Sans Serif 'I' equal what GDI+ returned on Windows to 0.01 font units; holes get smaller; the strength for 256 to 4096 units per em; italic and bold of the regular-only Liberation Sans. SyntheticStyleGdiTests compares the synthetic styles with GDI+ on Windows for several families. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Make the quadratic TrueType outline wider for synthetic bold The first Windows comparison showed the straight edges equal to GDI+, but curves off by a few font units ('o') and some glyphs by up to 35 ('8', serifs of Sylfaen and Gabriola). GDI+ makes the glyph wider on the outline of the font, before its quadratic segments become cubic ones: the points it moves and the limit by the shorter edge depend on that point set. The emboldening now works on the quadratic TrueType contours in font units; a CFF outline is made wider after it was drawn, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Keep the synthetic bold shift within half the strength per axis like GDI+ Measured point by point against GDI+ on the Windows runner: GDI+ does not limit the shift at an inner corner by the length of the shorter edge, as FT_Outline_EmboldenXY does, so the short edges of the rounded serif corners of Sylfaen move by the whole strength. Instead it keeps the shift of every point within half the strength on each axis, so that the sharp corners of the '8' of Microsoft Sans Serif do not shoot out by up to 24 font units. The comparison test now matches the points of the managed and the GDI+ outline by position, because GDI+ starts the contours of MS Gothic at another point. Refs #412 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Compare Sylfaen and Gabriola with GDI+ within 2% of the em height GDI+ moves single points of these two families differently from the other five, without a rule that could be found (e.g. a point in the middle of a straight slanted edge of the Sylfaen '8' only moves up). The differences are up to 1.6% and 0.85% of the em height; the other families keep the tolerance of 2 font units. Refs #412 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 * Make the GDI font provider the default in CADability.Forms 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 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5 --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Two crash fixes for 1.7.0 and the version bump.
#486: collect connected faces without recursion
Make3D.extractConnectedFaces,BRepOperation.extractConnectedFacesandBRepSelfIntersection.collectFaceswalked the faces of a shell with a recursive depth-first search that nested once per connected face. A DXF mesh of about 7000 triangles (sewing) or a boolean operation on a shell with some ten thousand faces overflowed the stack, and aStackOverflowExceptionterminates the process.The three traversals now use an explicit stack. They visit the faces in the same order as the recursion did, so the shells come out unchanged.
#487: cut reference cycles in the STEP import
ImportStep.CreateEntitycreates referenced entities recursively. It only knew that an entity was created once creation had finished. A reference cycle therefore re-enteredCreateEntityfor the same entity until the stack overflowed (the monitor is re-entrant). An example of such a cycle is aMAPPED_ITEMwhoseREPRESENTATION_MAPmaps the representation that contains the mapped item.StepImport.Parallel(the default), two threads can enter a cycle from different ends and then wait for each other's lock forever. This is the limitation noted in the issue. A thread that has to wait for an item now follows the chain of threads that wait for each other. When the chain leads back to an item the thread is creating itself, it cuts the cycle the same way instead of waiting.Version
<Version>is set to 1.7.0. A minor bump fits because the release adds public API since 1.6.0:IJsonProgress,DXF.IDxfProgressand the progress overloads (Show a progress bar during file open #367)Text.ClearFontCache(Public way to release the glyph display lists of FontCache (renderer switch, graphics device loss) #494)ProjectedModel.InvalidateDisplayLists(Lightweight way to make a ProjectedModel rebuild its display lists #496)PaintTo3DOutOfMemoryconstructors (PaintTo3DOutOfMemory cannot be thrown by IPaintTo3D implementations outside CADability #495)This PR adds no public API.
Tests
DeepFaceTraversalTestsruns two operations on a thread with a 256 KB stack:Without the fix, both crash the test host with a stack overflow.
StepCyclicReferenceTestimportsissue101.stpin two variants, sequentially and in parallel:Local results on Linux:
CADability.Tests: 636 passed, 19 skipped, 0 failed.CADability.ImportTests: 85 passed, 5 skipped, 0 failed.Closes #486, closes #487.
Generated by Claude Code