Skip to content

[Draft, do not merge] Integration branch for #412 - #467

Draft
dsn27 wants to merge 49 commits into
masterfrom
integration/issue-412
Draft

dsn27 wants to merge 49 commits into
masterfrom
integration/issue-412

Conversation

@dsn27

@dsn27 dsn27 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Integration branch for #412. Do not merge.

This draft exists so that the Linux and Windows CI run on the combined state of all open #412 pull requests before any of them reaches master. The CI only runs for pull requests and for pushes to master.

Contents

integration/issue-412 is the current master plus merge commits of:

#484 (run the kernel as WebAssembly in the Linux CI) targets this branch, because it needs both the font and the picture stacks. It is retargeted to master once those are merged.

The individual pull requests stay the place for review. New #412 work is merged in here as well, so the combination is always tested.

Merge notes

Local check (Linux, latest merge)

🤖 Generated with Claude Code

https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

First part of step 6 of #412. Text gets its glyphs from GDI today
(GraphicsPath.AddString, gdi32 for widths and kerning), which does not
exist outside Windows. The new internal class TrueTypeFont reads font
files itself:

- .ttf files and every font of a .ttc collection; CFF-based .otf and
  bitmap-only fonts are recognized and report HasTrueTypeOutlines=false
- glyph contours from glyf/loca with composite glyphs resolved (offsets,
  scales, 2x2 transforms, point matching)
- cmap formats 0, 4, 6 and 12, including symbol fonts
- advance widths (hmtx), metrics (head, hhea, OS/2) and names (name)
- kerning pairs of the version 0 kern table, as GDI reads them
- GetPathData returns the outline in the layout of a GDI+ GraphicsPath
  after AddString, so GlyphShapeBuilder.PathDataToPath2D can take it
  unchanged; the quadratic segments become cubic ones by exact degree
  elevation

Nothing uses it yet: the font service that replaces GDI in Text and
FontCache follows in the next pull request.

The tests compare names, metrics, glyph indices, advance widths, all
contours of 112 characters and the kerning with reference data that
fontTools wrote from the same font (Liberation Sans, SIL OFL 1.1, added
under tests/CADability.Tests/Files/Fonts together with the script that
wrote the reference). They also check that the outline is exactly the
quadratic curves, a font collection, and invalid input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
…DI (#412)

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

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

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
#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
…heckout

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
)

Step 6, part 2b of #412. The managed reader of #466 skipped OpenType
fonts with CFF outlines, which many .otf files have, so a text in such
a font fell back to the default face. The new internal CffOutlines
reads the "CFF " table:

- INDEX and DICT structures, the FontMatrix (which is stored rounded,
  e.g. 0.00048828 for 1/2048)
- the Type 2 charstring interpreter: all path operators including the
  specialized curves and lines, flex, hflex, hflex1, flex1, local and
  global subroutines with their bias, widths, hints and hint masks
  (skipped, they do not change the outline), the arithmetic operators
- CID-keyed fonts with FDArray and FDSelect (formats 0 and 3)

The cubic curves go into the GDI+ path data unchanged. Not supported:
CFF2 (variable OpenType fonts) and the deprecated accent composition of
endchar (seac). TrueTypeFont gets HasCffOutlines and HasOutlines, and
ManagedFontProvider indexes .otf and .otc files too.

Tests: make_cff_test_fonts.py makes two small test fonts from a subset
of Inter (SIL OFL 1.1): InterCffTest.otf rewritten to use subroutines,
the specialized operators, flex and hint masks, and InterCidTest.otf,
the same glyphs as a CID-keyed font with two font dicts. The outlines
of both are compared command by command with fontTools. Checked
locally against fontTools on 18,672 glyphs of the 47 CFF fonts of the
Linux container, 7 of them CID-keyed: all equal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The managed font reader now handles variable OpenType fonts. The named
instances of a variable font become faces of their own, with family names
as GDI makes them ("Bahnschrift Light Condensed", "Segoe UI Variable
Display Semibold"), so these fonts no longer fall back to their default
instance or, for CFF2, disappear from the font list.

- FontVariations parses fvar, avar, the gvar header and HVAR, normalizes
  user coordinates and computes tuple scalars like fontTools'
  supportScalar (peak and intermediate regions).
- TrueType outlines: gvar deltas with shared and private point numbers,
  packed deltas, interpolation of untouched points (IUP), phantom points
  and the offsets of composite components.
- CFF2 outlines: 32-bit INDEX counts, the Top DICT outside an INDEX,
  FDSelect format 4, vsindex and blend with the ItemVariationStore.
- Advance widths come from HVAR, or from the phantom points without HVAR.
  They are not rounded. MVAR is not applied.

The test fonts are subsets of Source Sans 3 VF (OFL-1.1), renamed to
"VarTest Sans" because "Source" is a Reserved Font Name. Their reference
outlines and advance widths were written with fontTools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
GdiFontComparisonTests checks on the fonts installed on Windows that the
ManagedFontProvider, the default since the font service, gives the same
results as the GdiFontProvider, which is how CADability got its fonts
before:

- every family GDI+ knows is known to the managed provider;
- for a list of common fonts (TrueType, a collection and variable fonts),
  in every real style: the metrics, the advance widths (within one font
  unit) and the outlines (Hausdorff distance within two font units),
  including underline and strikeout;
- for all installed families: the metrics, the advance widths and the
  outlines of a few characters.

The tests only compile for net8.0-windows and run in the Windows CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
GDI cuts family names to 31 characters (LF_FACESIZE), e.g. "Bahnschrift
SemiBold SemiConden", and texts saved with GDI keep that name. The
ManagedFontProvider now resolves such a name to the only family whose
name starts with it. The comparison with GDI checks that every family
name of GDI resolves to a face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The comparison with GDI on Windows found three differences, which a
diagnostic run confirmed against the raw font tables:

- Fonts that set USE_TYPO_METRICS (bit 7 of fsSelection) get their cell
  and line spacing from the typographic metrics of OS/2 in GDI+, not from
  usWinAscent and usWinDescent (Bahnschrift, Cascadia, Sitka, Cambria
  Math, Gabriola, Nirmala Text).
- GDI+ centers the underline and the strikeout line on the positions of
  "post" and "OS/2"; they were drawn below them.
- When xMin of a glyph is not its left side bearing in "hmtx", Windows,
  FreeType and fontTools move the outline so that the origin is at 0
  (Times New Roman Italic, Symbol). The varied origin of a variable font
  is taken into account.

The Windows test now compares the outlines with the unhinted outlines of
GDI (GetGlyphOutline with GGO_UNHINTED): GraphicsPath.AddString returns
hinted outlines, which differ from the design by a few font units in
strongly hinted fonts and are wrong for Segoe Script, while the managed
reader matches the unhinted outlines exactly. The underline and strikeout
lines are still compared with GDI+.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
The comparison with GDI on Windows found more differences:

- Windows names the instances of a variable font from "STAT": the names
  of the axis values in the axis ordering of "STAT", without the elidable
  ones. Segoe UI Variable has the instance "Light Display" in "fvar",
  which Windows calls "Segoe UI Variable Display Light". Without a usable
  "STAT" table the subfamily name of "fvar" is used as before.
- "MVAR" is now applied to the metrics of an instance (typographic and
  Windows ascent and descent, underline, strikeout). Bahnschrift varies
  the size of its strikeout line.
- A composite glyph with USE_MY_METRICS takes its origin from that
  component (Times New Roman Bold 'Ü').

The Windows test creates the GDI font with FW_DONTCARE for a regular
face, because FW_NORMAL makes GDI embolden a lighter face such as
"Cascadia Code ExtraLight".

The strikeout values of the variable test fonts at their instances were
added to the reference data with fontTools' instancer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
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
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
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
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
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
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
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
…oded-images

# Conflicts:
#	CADability/Bitmap.cs
#	CADability/ConstrPicture.cs
#	CADability/ConstrPicturePoint2Directions.cs
#	CADability/ConstrPicturePointWidthHeight.cs
…ge-setup

# Conflicts:
#	CADability/Project.cs
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
@dsn27
dsn27 force-pushed the integration/issue-412 branch from a0e12b1 to 28591f9 Compare October 16, 2026 08:14
claude added 20 commits October 16, 2026 08:21
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
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
Resolves the conflict with #497 in FontCache: the managed font calls of
this branch now run under the same lock (this) that #497 introduced, and
Clear disposes the glyph display lists and empties all caches, including
the faces. FontProvider.Current, which calls Clear, documents that the
display lists of the views need to be rebuilt when it is set after texts
were painted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5
…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
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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants