Skip to content

Make CADability.dll platform independent: remove GDI+ from the core, porting the ShapeIt work #412

Open

Description

@dsn27

Part of the CADability 2.0 discussion in #256 ("Better separation of GUI and Math Kernel"). The proper separation of kernel and UI is tracked separately in #413.

Summary

Goal: CADability runs in the browser, without causing problems for existing customers. Breaking changes are kept to a minimum, and where one cannot be avoided it must be easy to fix in code. In particular, customers who implement IPaintTo3D themselves (one customer built their whole rendering on it) must not be locked out.

Supporting the web or other platforms does not require splitting the math kernel from the UI first. CADability.dll is already independent of any UI framework: it targets netstandard2.0 and has no reference to System.Windows.Forms. All WinForms code lives in CADability.Forms. IFrame, IView, ICanvas, IUIService, IPropertyEntry and the menu types are platform-neutral interfaces that another front end can implement.

The real platform blocker is GDI+ (System.Drawing.Common), which has been Windows-only since .NET 7. But only creating GDI+ objects fails outside Windows; GDI+ types that merely appear in signatures do no harm (shown by #484).

So the browser runs on 1.x: the kernel no longer needs GDI+ at runtime, while the package reference and the GDI+ types in existing signatures stay. Dropping System.Drawing.Common altogether (step 8) is the only breaking part. It is deferred and is not a prerequisite for the browser.

ShapeIt has already done much of this work. Its copy of CADability builds without System.Drawing.Common, and it runs in a desktop (Avalonia) and a browser (Avalonia WebAssembly) front end. This issue ports that work topic by topic, keeping what works and correcting the problems listed below.

Prior work in ShapeIt

ShapeIt (develop, cd3cd8d) diverged from CADability at v1.0.22 (2025-08-20). Since then, 190 commits on master and 274 commits in ShapeIt changed the kernel, and 234 files were changed on both sides; ShapeIt still uses netDxf where master uses ACadSharp. Porting it is therefore a manual job by topic, which is already established practice (#359).

The relevant commits:

  • 82d54ba "Removed all references to System.Drawing from CADability" (257 files, mixed with unrelated geometry changes, so it cannot be cherry-picked)
  • 76e05ff IPaintTo3D and IUIService without System.Drawing types
  • bd18e3d glyph outlines via SkiaSharp in the Avalonia front end
  • 86c95af ShapeIt in the browser (Avalonia WebAssembly, WebGL2)

Worth porting

  • Old files stay readable: JsonSerialize still converts a serialized System.Drawing.Color. ShapeIt's kernel reads all 53 of its own test files and 4 of the 5 CADability test files on Linux.
  • RenamedTypes: a documented table that maps old serialized class names to current ones. It is not needed for this issue, but it is exactly what CADability 2.0: separate the geometry kernel from the UI layer (CADability.Core) #413 needs.
  • Fonts behind an abstraction: an abstract FontFamily with GetOutline2D() and real font metrics, with implementations based on GDI (WinForms) and SkiaSharp (Avalonia).
  • Printing moved into the WinForms layer.
  • Not ported: ShapeIt decodes images with StbImageSharp. That package is not strong-name signed, while CADability and all its dependencies are; as a dependency it would stop CADability from loading in .NET Framework applications. See step 7.

Problems to avoid when porting

These were reproduced on Linux with ShapeIt's kernel unless marked otherwise.

  1. Text does not work outside Windows and crashes the process.
    • The FontCache constructor still calls Gdi.CreateCompatibleDC from gdi32.dll. On Linux this throws a TypeInitializationException.
    • The finalizer then calls Gdi.DeleteDC and throws on the finalizer thread, which terminates the process (exit code 134).
    • Without a frame, Text already fails earlier with a NullReferenceException, because the kernel gets fonts through FrameImpl.MainFrame as IUIService.
    • The browser runs the same code path; this was not tried there.
  2. The geometry depends on the front end. The same text becomes B-spline glyphs under WinForms and flattened polylines under Avalonia, so a model's geometry differs depending on the application that opens it. The Avalonia GetExtent also ignores the font style.
  3. The substitute Color behaves differently from System.Drawing.Color.
    • Color.Red.Name is empty and IsNamedColor is false; FromName("Red") loses the name.
    • Fully transparent black is IsEmpty and equal to Color.Empty.
    • FromString returns blue for every name outside a list of about 17. As a result, the SVG import turns navy, teal, silver, olive, maroon, lime and gold into blue, where ColorTranslator.FromHtml was correct before.
  4. The public API breaks more than necessary. Color, Point, PointF, Rectangle, RectangleF and Size are part of netstandard2.0 and work on every platform, including WebAssembly. ShapeIt replaced them with its own CADability.Substitutes types anyway.
    • On master, 48 public types expose System.Drawing types in their signatures: Rectangle 15 times, GraphicsPath 13, Point 10, Color 9, Bitmap 6.
    • Replacing all of them breaks every consumer, including implementers of IPaintTo3D and IUIService.
    • The substitute Rectangle.IsEmpty also has a different meaning.
  5. Images (from reading the code, not tested):
    • The binary format no longer stores the bitmap; it is reloaded from its original path, and the picture is silently empty when that file is gone.
    • JSON stores raw RGBA as Base64. A 12-megapixel photo becomes about 64 MB.
    • Picture.CopyFrom does not dispose the file stream.
  6. Features were commented out instead of replaced: AddToGraphicsPath (public API), PrintToGDI and printing in LayoutView.
  7. No safety net. There are no tests for the substitute types. ShapeIt's tests target net8.0-windows, and its CI only builds and packs on Windows, which is why problem 1 went unnoticed.

Findings on master

Based on a type dependency graph of the compiled CADability.dll (Mono.Cecil, all 767 top-level types including method bodies) on master cfe9d3c, and a build and test run on Linux.

  • The geometry already runs without Windows. On Linux, CADability.Tests passes 480 of 500 tests (18 skipped) and CADability.ImportTests passes 74 of 79 (5 skipped).

    • The 2 failures (VolumesMatchAnalytic, IntegratedVolumeMatchesAnalyticOnEveryMesh) happen while reading Volumes.cdb.json. The exceptions are swallowed in JsonSerialize.CreateDeserializer: InvalidCastException in LayerList and SphericalSurface, and SerializationException (Member 'NamedValues' was not found) in NamedValuesProperty.
    • The file contains no System.Drawing types, and ShapeIt's kernel reads it on Linux. Whether the tests also fail on Windows has not been checked.
  • 49 types use GDI+ from System.Drawing.Common:

    Area Types GDI+ usage
    2D curves and shapes ICurve2D and all implementations, Border, SimpleShape, CompoundShape, Curve2DAspect AddToGraphicsPath(GraphicsPath, …)
    Transformations ModOp.Matrix2D, ModOp2D.Matrix2D conversion to Drawing2D.Matrix, only used by PrintToGDI and CADability.Forms/PaintToGDI
    Text Text, FontCache, GlyphShapeBuilder, DimensionStyle, ShowPropertyFont glyph outlines via GraphicsPath.AddString, glyph widths via gdi32 (Gdi.GetCharABCWidths)
    Images Picture, Icon, ShowPropertyPicture, ConstrPicture*, IPaintTo3D Bitmap, Image
    Printing Project, Layout, LayoutView, GDI2DView, PrintToGDI, Print*, IUIService PrintDocument, PageSettings, both also written into project files
  • The WebDebug configuration (WEBASSEMBLY, substitute types in WebDrawing.cs, 78 conditional files) currently fails with 4 compile errors:

    • ImportSVG.cs(13): using static CADability.PrintToGDI;
    • FormsSubst.cs(60): Graphics not found
    • ImportDxf.cs(508) and ImportDxf.cs(528): Color is ambiguous between CADability.WebDrawing.Color and ACadSharp.Color
  • There is no CI that builds or tests on Linux. The only workflows are nuget.yml (Windows) and pages.yml.

Proposed steps

Each step is a separate, small pull request.

Steps 1–7 and 9 stay in 1.x and must not break the public API:

  • Classes keep every replaced member, marked [Obsolete], forwarding to its replacement and naming it in the message.
  • Public interfaces (IPaintTo3D, IUIService, ICurve2D, IPropertyTreeView) gain no members and lose none. netstandard2.0 has no default interface methods, so every added member breaks all implementers, and [Obsolete] does not help there.
  • Where a new path is needed, add an optional interface that the kernel checks with is, and keep the old members.

Step 8 is the only breaking step. It is deferred and is not needed for the browser.

  1. Linux CI.
    • Add a workflow on ubuntu-latest that builds CADability.csproj and runs CADability.Tests and CADability.ImportTests on every pull request.
    • The ACadSharp submodule needs submodules: recursive, because CSUtilities is a nested submodule.
    • This comes first, so that every following step is checked on a platform without GDI+.
  2. Investigate the 2 Linux test failures. Check whether they also fail on Windows. Make the exception swallowed in JsonSerialize visible, so a failing entity points at its cause instead of a later NullReferenceException.
  3. Decide about WebDebug. A single build for all platforms makes it obsolete. Fix its 4 compile errors only if it is useful as a guard; otherwise delete it together with WebDrawing.cs.
  4. Keep the System.Drawing primitive types. Color, Point, PointF, Rectangle, RectangleF, Size and SizeF come from netstandard2.0 and stay in the API. This avoids problems 3 and 4 and keeps 1.x source and binary compatible.
  5. Move the GDI+ conversions out of the geometry types.
    • ICurve2D.AddToGraphicsPath and Border/SimpleShape/CompoundShape.AddToGraphicsPath become extension methods in CADability.Forms, together with ModOp.Matrix2D and ModOp2D.Matrix2D.
    • The old members stay, marked [Obsolete] and forwarding to the extensions. ShapeIt removed them without replacement (problem 6).
  6. Fonts as a kernel service, porting ShapeIt's FontFamily abstraction with these corrections:
    • The kernel gets the font provider from a static, settable service in the kernel, not through FrameImpl.MainFrame. Text then works headless (problem 1).
    • Glyph outlines are exact curves, independent of the provider. Bezier segments become BSpline2D, as ShapeIt's WinForms implementation and master's GlyphShapeBuilder already do. Providers must not flatten (problem 2).
    • Nothing in the kernel calls gdi32 any more, including the FontCache constructor and finalizer.
    • The GDI provider moves to CADability.Forms. ShapeIt's SkiaSharp provider can serve other front ends.
    • A managed TrueType/OpenType reader in the kernel provides text on every platform. In the browser there are no system fonts, so the host registers font files as bytes.
  7. Images and printing.
    • Picture and Icon keep the encoded bytes of the image file (PNG, JPEG, GIF, BMP) and serialize those instead of a GDI+ Bitmap.
    • The kernel does not decode images. It reads the pixel size from the file header. A decoder package is ruled out, because StbImageSharp is not strong-name signed (see Worth porting).
    • Decoding happens in the renderer: with GDI+ on Windows, natively in the browser.
    • Optional image interface for renderers, checked with is: it receives the encoded bytes, the pixel size and a cache key, so a renderer without GDI+ (for example in the browser) can show pictures and icons. Renderers that only implement IPaintTo3D keep the Bitmap path; without GDI+ they show the outline of a picture.
    • Old files keep loading on every platform. When the image of an old file cannot be read, the picture falls back to its path with a warning instead of silently becoming empty. Streams are disposed.
    • Printing moves into CADability.Forms as in ShapeIt. PageSettings stored in project files are read into a neutral record (paper size, margins, orientation), so those files open without GDI+.
  8. Deferred, optional (would be 2.0): drop the System.Drawing.Common package reference.
  9. Verification on non-Windows hosts.
    • The Linux CI from step 1 gets headless tests that create, measure and export a Text and a Picture, and checks that the process survives the finalizers.

    • WebAssembly smoke test (Run the kernel as WebAssembly in the Linux CI (#412) #484): CADability.dll runs as .NET WebAssembly under Node.js in the Linux CI, with 13 checks covering:

      • a font from bytes, a Boolean operation, text, a picture,
      • painting through a custom IPaintTo3D with the unchanged 1.x signature,
      • old 1.x files, STEP, DXF/STL and the finalizers.

      On master, 7 of them fail with PlatformNotSupportedException.

    • A separate web sample is not needed beyond that: ShapeIt's browser front end already shows a complete WebAssembly front end.

Breaking changes of step 8 (deferred)

39 public members of the current API use GDI+ types from System.Drawing.Common in their signatures (read from the compiled CADability.dll on master). Together with the package reference, they are the complete list of what step 8 would remove or change. None of them changes as long as step 8 is not done. The primitive types Color, Point, PointF, Rectangle, RectangleF, Size and SizeF stay in any case. The breaking changes of #413 are separate.

Interfaces: would break every implementer

Interface Members Implemented by
IPaintTo3D DisplayBitmap, DisplayIcon, PrepareBitmap (2 overloads), PrepareIcon, RectangularBitmap (all with Bitmap); PrepareText, Text (with FontStyle) every renderer
IUIService CreatePaintInterface(Bitmap, double), GetBitmap(string), ShowPageSetupDlg, ShowPrintDlg (with PrintDocument/PageSettings) every host application
ICurve2D AddToGraphicsPath custom 2D curves
IPropertyTreeView SetFont(Font) custom property views

FontStyle comes from System.Drawing.Common as well and would need an enum of the kernel.

Classes (all already [Obsolete] with a named replacement in 1.x)

  • AddToGraphicsPath on Arc2D, BSpline2D, Circle2D, Ellipse2D, EllipseArc2D, GeneralCurve2D, Line2D, Path2D, Polyline2D and Border, CompoundShape.CreateGraphicsPath() and ModOp2D.Matrix2D become extension methods in CADability.Forms.
  • Picture.Bitmap, Picture.Set(Bitmap, …), Picture.SetBitmapNoUndo(Bitmap) and Icon.Set(Bitmap, …): replaced by the members that take the image file bytes.
  • Layout.pageSettings (a public field of type PageSettings): replaced by the neutral page setup.
  • LayoutView.Print(PrintDocument), GDI2DView.PrintSinglePage(PrintDocument) and the class PrintToGDI move into CADability.Forms.
  • ModelView.GetContentsAsBitmap() and OpenGlCustomize.PaintToBitmap(…) would no longer return a GDI+ Bitmap.
  • Substitutes.PaintEventArgs.Graphics would be removed.

Behaviour and packaging

  • The package would no longer depend on System.Drawing.Common. Applications that got GDI+ only through CADability would have to reference it themselves; WinForms applications already have it.

Acceptance criteria

  • Nothing in the kernel needs GDI+ or gdi32 at runtime. Outside Windows, text, pictures, printing data and old files work without a PlatformNotSupportedException, and the process survives the finalizers.
  • CADability.dll runs as WebAssembly in CI (Run the kernel as WebAssembly in the Linux CI (#412) #484).
  • No 1.x release changes a public interface. Every replaced class member is [Obsolete] and names its replacement. The System.Drawing primitive types stay in the API.
  • Both test projects are green on Linux in CI, including:
    • headless Text on Linux without a frame,
    • identical glyph outlines (exact curves, within a tolerance) from the GDI provider and the provider used on Linux,
    • the SVG import of all CSS named colors.
  • Project files written by 1.x still open, including their page settings and embedded pictures.
  • Step 8 (dropping System.Drawing.Common) is not required to close this issue.

Activity

  1. changed the title [-]Make CADability.dll platform independent: remove GDI+ from the core and revive the WebAssembly build[/-] [+]Make CADability.dll platform independent: remove GDI+ from the core, porting the ShapeIt work[/+] on Oct 14, 2026
  2. 3 remaining items

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions