Repository navigation
Make CADability.dll platform independent: remove GDI+ from the core, porting the ShapeIt work #412
Description
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)76e05ffIPaintTo3DandIUIServicewithoutSystem.Drawingtypesbd18e3dglyph outlines via SkiaSharp in the Avalonia front end86c95afShapeIt in the browser (Avalonia WebAssembly, WebGL2)
Worth porting
- Old files stay readable:
JsonSerializestill converts a serializedSystem.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
FontFamilywithGetOutline2D()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.
- Text does not work outside Windows and crashes the process.
- The
FontCacheconstructor still callsGdi.CreateCompatibleDCfromgdi32.dll. On Linux this throws aTypeInitializationException. - The finalizer then calls
Gdi.DeleteDCand throws on the finalizer thread, which terminates the process (exit code 134). - Without a frame,
Textalready fails earlier with aNullReferenceException, because the kernel gets fonts throughFrameImpl.MainFrame as IUIService. - The browser runs the same code path; this was not tried there.
- The
- 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
GetExtentalso ignores the font style. - The substitute
Colorbehaves differently fromSystem.Drawing.Color.Color.Red.Nameis empty andIsNamedColorisfalse;FromName("Red")loses the name.- Fully transparent black is
IsEmptyand equal toColor.Empty. FromStringreturns blue for every name outside a list of about 17. As a result, the SVG import turnsnavy,teal,silver,olive,maroon,limeandgoldinto blue, whereColorTranslator.FromHtmlwas correct before.
- The public API breaks more than necessary.
Color,Point,PointF,Rectangle,RectangleFandSizeare part ofnetstandard2.0and work on every platform, including WebAssembly. ShapeIt replaced them with its ownCADability.Substitutestypes anyway.- On
master, 48 public types exposeSystem.Drawingtypes in their signatures:Rectangle15 times,GraphicsPath13,Point10,Color9,Bitmap6. - Replacing all of them breaks every consumer, including implementers of
IPaintTo3DandIUIService. - The substitute
Rectangle.IsEmptyalso has a different meaning.
- On
- 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.CopyFromdoes not dispose the file stream.
- Features were commented out instead of replaced:
AddToGraphicsPath(public API),PrintToGDIand printing inLayoutView. - 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.Testspasses 480 of 500 tests (18 skipped) andCADability.ImportTestspasses 74 of 79 (5 skipped).- The 2 failures (
VolumesMatchAnalytic,IntegratedVolumeMatchesAnalyticOnEveryMesh) happen while readingVolumes.cdb.json. The exceptions are swallowed inJsonSerialize.CreateDeserializer:InvalidCastExceptioninLayerListandSphericalSurface, andSerializationException(Member 'NamedValues' was not found) inNamedValuesProperty. - The file contains no
System.Drawingtypes, and ShapeIt's kernel reads it on Linux. Whether the tests also fail on Windows has not been checked.
- The 2 failures (
-
49 types use GDI+ from
System.Drawing.Common:Area Types GDI+ usage 2D curves and shapes ICurve2Dand all implementations,Border,SimpleShape,CompoundShape,Curve2DAspectAddToGraphicsPath(GraphicsPath, …)Transformations ModOp.Matrix2D,ModOp2D.Matrix2Dconversion to Drawing2D.Matrix, only used byPrintToGDIandCADability.Forms/PaintToGDIText Text,FontCache,GlyphShapeBuilder,DimensionStyle,ShowPropertyFontglyph outlines via GraphicsPath.AddString, glyph widths viagdi32(Gdi.GetCharABCWidths)Images Picture,Icon,ShowPropertyPicture,ConstrPicture*,IPaintTo3DBitmap,ImagePrinting Project,Layout,LayoutView,GDI2DView,PrintToGDI,Print*,IUIServicePrintDocument,PageSettings, both also written into project files -
The
WebDebugconfiguration (WEBASSEMBLY, substitute types inWebDrawing.cs, 78 conditional files) currently fails with 4 compile errors:ImportSVG.cs(13):using static CADability.PrintToGDI;FormsSubst.cs(60):Graphicsnot foundImportDxf.cs(508)andImportDxf.cs(528):Coloris ambiguous betweenCADability.WebDrawing.ColorandACadSharp.Color
-
There is no CI that builds or tests on Linux. The only workflows are
nuget.yml(Windows) andpages.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.0has 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.
- Linux CI.
- Add a workflow on
ubuntu-latestthat buildsCADability.csprojand runsCADability.TestsandCADability.ImportTestson every pull request. - The ACadSharp submodule needs
submodules: recursive, becauseCSUtilitiesis a nested submodule. - This comes first, so that every following step is checked on a platform without GDI+.
- Add a workflow on
- Investigate the 2 Linux test failures. Check whether they also fail on Windows. Make the exception swallowed in
JsonSerializevisible, so a failing entity points at its cause instead of a laterNullReferenceException. - 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 withWebDrawing.cs. - Keep the
System.Drawingprimitive types.Color,Point,PointF,Rectangle,RectangleF,SizeandSizeFcome fromnetstandard2.0and stay in the API. This avoids problems 3 and 4 and keeps 1.x source and binary compatible. - Move the GDI+ conversions out of the geometry types.
ICurve2D.AddToGraphicsPathandBorder/SimpleShape/CompoundShape.AddToGraphicsPathbecome extension methods inCADability.Forms, together withModOp.Matrix2DandModOp2D.Matrix2D.- The old members stay, marked
[Obsolete]and forwarding to the extensions. ShapeIt removed them without replacement (problem 6).
- Fonts as a kernel service, porting ShapeIt's
FontFamilyabstraction with these corrections:- The kernel gets the font provider from a static, settable service in the kernel, not through
FrameImpl.MainFrame.Textthen works headless (problem 1). - Glyph outlines are exact curves, independent of the provider. Bezier segments become
BSpline2D, as ShapeIt's WinForms implementation andmaster'sGlyphShapeBuilderalready do. Providers must not flatten (problem 2). - Nothing in the kernel calls
gdi32any more, including theFontCacheconstructor 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.
- The kernel gets the font provider from a static, settable service in the kernel, not through
- Images and printing.
PictureandIconkeep 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
StbImageSharpis 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 implementIPaintTo3Dkeep theBitmappath; 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.Formsas in ShapeIt.PageSettingsstored in project files are read into a neutral record (paper size, margins, orientation), so those files open without GDI+.
- Deferred, optional (would be 2.0): drop the
System.Drawing.Commonpackage reference.- Removes the API listed under Breaking changes of step 8 below.
- Not needed for the browser (Run the kernel as WebAssembly in the Linux CI (#412) #484). Do it only if a later major release wants this cleanup, and weigh it against customers who implement
IPaintTo3DorIUIService.
- Verification on non-Windows hosts.
-
The Linux CI from step 1 gets headless tests that create, measure and export a
Textand aPicture, and checks that the process survives the finalizers. -
WebAssembly smoke test (Run the kernel as WebAssembly in the Linux CI (#412) #484):
CADability.dllruns 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
IPaintTo3Dwith the unchanged 1.x signature, - old 1.x files, STEP, DXF/STL and the finalizers.
On
master, 7 of them fail withPlatformNotSupportedException. -
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)
AddToGraphicsPathonArc2D,BSpline2D,Circle2D,Ellipse2D,EllipseArc2D,GeneralCurve2D,Line2D,Path2D,Polyline2DandBorder,CompoundShape.CreateGraphicsPath()andModOp2D.Matrix2Dbecome extension methods inCADability.Forms.Picture.Bitmap,Picture.Set(Bitmap, …),Picture.SetBitmapNoUndo(Bitmap)andIcon.Set(Bitmap, …): replaced by the members that take the image file bytes.Layout.pageSettings(a public field of typePageSettings): replaced by the neutral page setup.LayoutView.Print(PrintDocument),GDI2DView.PrintSinglePage(PrintDocument)and the classPrintToGDImove intoCADability.Forms.ModelView.GetContentsAsBitmap()andOpenGlCustomize.PaintToBitmap(…)would no longer return a GDI+Bitmap.Substitutes.PaintEventArgs.Graphicswould 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
gdi32at runtime. Outside Windows, text, pictures, printing data and old files work without aPlatformNotSupportedException, and the process survives the finalizers. CADability.dllruns 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. TheSystem.Drawingprimitive types stay in the API. - Both test projects are green on Linux in CI, including:
- headless
Texton 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.
- headless
- 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
[-]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[/+]Add a Linux CI workflow that builds and tests the kernel (#412) (#461)
Find a family by the name GDI cuts to 31 characters (#412)
Lay out glyphs, metrics and decorations like GDI (#412)
Correct the documentation of HasOutlines and HasCffOutlines for CFF2 (#…
Name and measure variable instances like Windows (#412)
Test files with pictures written on .NET Framework 4.8 (#412)
Use a relative path in the path fallback test (#412)
Keep the cause when JsonSerialize cannot create an entity (#412) (#463)
Remove the WebDebug configuration and its WEBASSEMBLY code (#412) (#464)
Import all CSS named colors from SVG (#412) (#478)
Run the kernel as WebAssembly in the Linux CI (#412)
Keep Picture.Clip without [Obsolete] (#412)
Keep the printing members without [Obsolete] (#412)
3 remaining items
Pass Unicode code points instead of chars to FontFace (#412)
Keep the synthetic bold shift within half the strength per axis like …
Compare Sylfaen and Gabriola with GDI+ within 2% of the em height
Make the GDI font provider the default in CADability.Forms (#503)
Add a managed TrueType reader for text on every platform (#466)
Keep the images of pictures and icons as image file bytes (#412, step…
Get the fonts of texts from a font service of the kernel instead of G…
Save page settings as a neutral PageSetup (#412, step 7b) (#474)
Read the CFF outlines of OpenType fonts in the managed font reader (#470
Test pictures without frame or GDI: measure, export, finalizers (#412,…
Read variable fonts: gvar, CFF2, avar and HVAR (#412, step 6) (#471)
Compare the managed font reader with GDI on Windows and fix the diffe…
Synthesize a missing bold or italic face like GDI+ (#412) (#500)
Test texts without frame or GDI: save, export, finalizers (#412, step…
Run the kernel as WebAssembly in the Linux CI (#412) (#484)