Skip to content

Add SkiaSharp.Extended.Drawing.Common — cross-platform System.Drawing.Common replacement#388

Draft
mattleibow wants to merge 84 commits into
mainfrom
mattleibow/system-drawing-wrapper
Draft

Add SkiaSharp.Extended.Drawing.Common — cross-platform System.Drawing.Common replacement#388
mattleibow wants to merge 84 commits into
mainfrom
mattleibow/system-drawing-wrapper

Conversation

@mattleibow
Copy link
Copy Markdown
Collaborator

@mattleibow mattleibow commented May 1, 2026

SkiaSharp.Extended.Drawing.Common

A cross-platform replacement for System.Drawing.Common backed entirely by SkiaSharp. Same namespace, same API surface — reference the NuGet and existing System.Drawing code runs on macOS, Linux, iOS, Android, and everywhere SkiaSharp runs.

Why?

System.Drawing.Common is Windows-only on .NET 6+. Thousands of apps and libraries depend on its API for 2D rendering, image manipulation, and printing. This package provides the same API backed by SkiaSharp so that code written against System.Drawing continues to work without rewriting.

How It Works

All types live in the System.Drawing / System.Drawing.Drawing2D / System.Drawing.Imaging / System.Drawing.Text / System.Drawing.Printing namespaces, exactly matching the original. Under the hood, every type delegates to SkiaSharp:

System.Drawing Type SkiaSharp Backing Notes
Graphics SKCanvas Full Draw/Fill surface with transforms, clipping, state save/restore
Bitmap / Image SKBitmap Load/save PNG/JPEG/BMP/GIF, pixel access, LockBits, RotateFlip
Pen SKPaint (stroke) All line caps, joins, dash styles, compound arrays, brush-backed
SolidBrush, TextureBrush, LinearGradientBrush, HatchBrush, PathGradientBrush SKPaint / SKShader All 53 hatch patterns, gradient blends, wrap modes
GraphicsPath SKPath Lines, curves, arcs, beziers, text outlines, Flatten, Widen
Matrix SKMatrix All affine transforms with MatrixOrder support
Font / FontFamily SKFont / SKTypeface DrawString with word wrapping, MeasureString
Region SKPath + SKPath.Op All boolean combine modes (Union, Intersect, Exclude, Complement, Xor)
PrintDocument SKDocument.CreatePdf Standard PrintDocument event model → PDF output
Brushes / Pens Lazy cached All 141 named colors, immutable system instances

GDI+ Coordinate Compatibility

GDI+ has a quirk: it offsets curve coordinates by +0.5 pixels for rasterization. We replicate this exactly via GdiCurveRect() and GdiPolygonPath() helpers, bringing pixel error from ~3% down to < 0.1% for solid fills and < 0.5% for non-antialiased geometry.

Rendering Category Pixel Error vs Real GDI+
Solid fills and colors < 0.1%
Lines, rectangles, stroked shapes < 0.5%
Ellipses, arcs, pies, polygons < 0.5%
Anti-aliased geometry < 5% (different AA algorithms)

Benchmarks (GDI+ vs SkiaSharp)

Measured on Windows CI with BenchmarkDotNet (ShortRun, .NET 10.0.7, X64 RyuJIT AVX2). Operations are 1000 iterations over a 500×500 bitmap.

Skia Faster

Operation GDI+ Skia Speedup
FillRectangle 8,737 μs 648 μs 13.5×
FillEllipse 9,181 μs 743 μs 12.4×
Clear 27,222 μs 2,327 μs 11.7×
CreateDispose (Bitmap) 7,091 μs 658 μs 10.8×
DrawString 4,271 μs 593 μs 7.2×
GetPixel 2,233 μs 504 μs 4.4×
DrawRectangle 771 μs 227 μs 3.4×
PathFill 5,123 μs 2,093 μs 2.4×
DrawLine 2,392 μs 1,482 μs 1.6×
DrawBezier 578 μs 466 μs 1.2×
MeasureString 660 μs 799 μs ~same

GDI+ Faster

Operation GDI+ Skia Ratio
SetPixel 2,223 μs 18,780 μs 8.5× slower
SavePng 5,277 μs 12,354 μs 2.3× slower
DrawEllipse (stroke) 4,980 μs 7,112 μs 1.4× slower

API Coverage

  • 92.3% of the API surface is implemented (1,552 / 1,682 members)
  • 130 remaining stubs throw PlatformNotSupportedException — these are all genuinely Windows-only: HDC/HWND handles, EMF/WMF metafiles, CopyFromScreen, GDI+ object handles
  • API compatibility validated on every commit via dotnet apicompat --strict-mode

Pixel Validation

Every drawing feature has paired test scenarios that render the same code through both real System.Drawing (GDI+ on Windows) and our SkiaSharp wrapper. The outputs are diffed pixel-by-pixel — 121 scenarios across 26 categories with 242 reference images checked in.

Known Limitations

See KNOWN-LIMITATIONS.md for the full list of unimplemented methods, partial implementations (ImageAttributes gamma/threshold, character-level text trimming, sub-byte LockBits formats), and cross-platform rendering differences.

mattleibow and others added 14 commits April 30, 2026 21:52
- Generated API stubs from System.Drawing.Common 10.0.2 (netstandard2.0)
  using Microsoft.DotNet.GenAPI.Tool
- 3,041 lines covering 148 types across 6 namespaces
- All method bodies throw PlatformNotSupportedException
- Added Microsoft.DotNet.ApiCompat.Tool for CI validation
- Added baseline assembly in tools/api-baseline/

Note: Generated stubs have compile errors that need fixing:
- NullableAttribute/NullableContextAttribute removed (compiler-managed)
- Missing interface implementations (ISerializable, ICollection)
- Protected Finalize on sealed classes
- DefaultMember attribute conflicts with indexers
- SupportedOSPlatformAttribute missing on netstandard2.0

Co-authored-by: Copilot <[email protected]>
146 files across 6 namespace directories:
- System/Drawing/ (43 files)
- System/Drawing/Design/ (1 file)
- System/Drawing/Drawing2D/ (36 files)
- System/Drawing/Imaging/ (33 files)
- System/Drawing/Printing/ (27 files)
- System/Drawing/Text/ (6 files)

Co-authored-by: Copilot <[email protected]>
Fixes applied to GenAPI-generated stubs:
- Remove protected modifier from destructors (~ClassName)
- Remove DefaultMemberAttribute on types with indexers
- Add ISerializable.GetObjectData to Font, Icon, Image
- Add IDisposable.Dispose to FontConverter.FontNameConverter
- Add ICollection members to PrinterSettings collections
- Remove RequiresUnreferencedCodeAttribute (netstandard2.0 compat)
- Remove SupportedOSPlatformAttribute (netstandard2.0 compat)
- Fix delegate signatures (DrawImageAbort, EnumerateMetafileProc,
  PlayRecordCallback, PrintEventHandler, PrintPageEventHandler,
  QueryPageSettingsEventHandler)
- Add internal constructors to suppress public defaults on types
  with no visible constructors in baseline
- Fix Image/FontCollection protected Dispose(bool) visibility
- Set AssemblyVersion to 10.0.0.0 to match baseline

ApiCompat result: PASS (only public key token suppressed)

Co-authored-by: Copilot <[email protected]>
- Add api_compat job to azure-pipelines-public.yml that runs
  dotnet apicompat --strict-mode against the baseline assembly
- Add dotnet-eng NuGet feed for GenAPI tool restore
- Add README.md documenting the API baseline and update process

Co-authored-by: Copilot <[email protected]>
- SkiaSharp.Drawing.Tests project with xUnit v3
- PixelCompatibilityTestBase with tolerance tiers, reference image
  comparison via SKPixelComparer, and diff visualization on failure
- ApiSurfaceTests validating 16 core System.Drawing types exist
  with correct assembly name and PNSE behavior
- All 18 tests pass

Co-authored-by: Copilot <[email protected]>
- Add pixel_compat job to azure-pipelines-public.yml running
  SkiaSharp.Drawing.Tests with test result publishing
- Add SkiaSharp.Drawing.Tests to solution file
- Both api_compat and pixel_compat run after test job

Co-authored-by: Copilot <[email protected]>
Image:
- SKBitmap internal backing store
- Width/Height/Size/PixelFormat properties
- FromFile/FromStream static factories using SKBitmap.Decode
- Save to file/stream with format detection
- Clone, RotateFlip, GetThumbnailImage
- Dispose pattern

Bitmap:
- All constructors (size, file, stream, copy, resize)
- GetPixel/SetPixel via SKBitmap
- SetResolution, MakeTransparent
- Clone with sub-region extraction
- LockBits/UnlockBits via pixel pointer

Also:
- Internal/SkiaConversions.cs for type mapping
- ImageFormat/BitmapData/ColorPalette/FrameDimension backing
- XML doc comments on all public members

Co-authored-by: Copilot <[email protected]>
Brush:
- Dispose pattern with tracking, internal CreatePaint()

SolidBrush:
- Color storage, SKPaint with Fill style

Pen:
- All properties (color, width, dash, caps, joins, miter)
- CreatePaint() maps to SKPaint with Stroke style
- LineCap/LineJoin/DashStyle → SKStrokeCap/SKStrokeJoin/SKPathEffect

TextureBrush:
- Image + WrapMode backing, SKShader.CreateBitmap

Updated test to verify SolidBrush construction works.
XML doc comments on all public members.

Co-authored-by: Copilot <[email protected]>
Implemented via SKCanvas:
- FromImage factory, Clear, Dispose, Flush
- DrawLine (4 overloads), DrawLines, DrawRectangle (3),
  DrawRectangles, DrawEllipse (4), DrawArc (4), DrawPie (4),
  DrawPolygon (2)
- FillRectangle (4), FillRectangles, FillEllipse (4),
  FillPie (3), FillPolygon (4)
- DrawImage (11 overloads), DrawImageUnscaled (4)
- TranslateTransform, ScaleTransform, RotateTransform, ResetTransform
- Save/Restore state management
- All state properties (SmoothingMode, InterpolationMode, etc.)

PNSE retained for: Font/text, GraphicsPath, Region/clipping,
Matrix, beziers, curves, metafiles, HDC/HWND

XML doc comments on all 240+ public members.
ApiCompat: PASS (zero breaking changes)

Co-authored-by: Copilot <[email protected]>
Test coverage across 7 test files (2,484 lines):
- BitmapTests (44): constructors, GetPixel/SetPixel, MakeTransparent,
  Clone, Save/Load roundtrips, LockBits, dispose
- ImageTests (39): FromFile/FromStream, properties, Clone, RotateFlip,
  GetThumbnailImage, dispose
- GraphicsTests (69): Clear, DrawLine/Rect/Ellipse/Arc/Pie/Polygon,
  FillRect/Ellipse/Pie/Polygon, DrawImage, transforms, Save/Restore
- PenTests (32): constructors, all properties, DashStyle, caps, Clone
- BrushTests (18): SolidBrush/TextureBrush construction, Clone, dispose
- ImageFormatTests (22): all GUIDs, Equals, GetHashCode, ToString
- SelfConsistencyTests (6): render→save→reload pixel comparison

Also:
- Fixed PixelCompatibilityTestBase.ConvertToSKBitmap to use internal backing
- Added InternalsVisibleTo for test project access
- ApiCompat: PASS

Co-authored-by: Copilot <[email protected]>
Shared scenarios library (46 scenarios across 10 categories):
- Clear, Lines, AA Lines, Rectangles, Ellipses, AA Ellipses,
  Arcs, Pies, Polygons, Composites, Colors

Reference generator (Windows-only, net9.0-windows):
- Uses real System.Drawing.Common (GDI+) to produce golden PNGs
- tools/SkiaSharp.Drawing.ReferenceGenerator/

Pixel comparison tests:
- SkiaDrawingSurface implements IDrawingSurface with our wrapper
- ReferenceComparisonTests compares against golden images per-scenario
- Gracefully skips when reference images absent (local dev)
- 46 theory tests with category-based tolerance tiers

CI pipeline:
- generate_references job produces golden PNGs on Windows
- pixel_compat job downloads and runs comparison tests

Test results: 265 passed, 46 skipped (no ref images locally)
ApiCompat: PASS

Co-authored-by: Copilot <[email protected]>
Remove IDrawingSurface abstraction. Since SkiaSharp.Drawing uses
the same System.Drawing namespace and API, the same source files
compile against both backends via <Compile Include>.

Shared scenarios (tools/SkiaSharp.Drawing.Scenarios/):
- DrawingScenarios.cs — 46 scenarios using System.Drawing directly
- ScenarioRunner.cs — Bitmap + Graphics.FromImage + Save

Two consumers compile the same files:
- ReferenceGenerator (net9.0-windows) → real System.Drawing/GDI+
- SkiaRunner + Tests (net10.0) → our SkiaSharp.Drawing wrapper

Deleted: IDrawingSurface, TestScenario, Scenarios.csproj,
SystemDrawingSurface, SkiaDrawingSurface

All checks pass: build ✓, 311 tests ✓, ApiCompat ✓

Co-authored-by: Copilot <[email protected]>
Generated 46 reference images via SkiaRunner for all scenarios:
Clear, Lines, AA Lines, Rectangles, Ellipses, AA Ellipses,
Arcs, Pies, Polygons, Composites, Colors

All 46 ReferenceComparisonTests now PASS with pixel comparison
(previously skipped). These compare rendered output against
checked-in baselines using SKPixelComparer.

On Windows CI, the ReferenceGenerator will produce GDI+ golden
images that replace these baselines for cross-backend validation.

Test results: 311 passed, 0 skipped, 0 failed

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 1, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

📖 Documentation Preview

The documentation for this PR has been deployed and is available at:

🔗 View Staging Documentation

🔗 View Staging Blazor Sample

This preview will be updated automatically when you push new commits to this PR.


This comment is automatically updated by the documentation staging workflow.

Reference images generated by ReferenceGenerator on Windows CI
(Build 157220) using real System.Drawing.Common/GDI+.

Pixel comparison results against GDI+ (22 pass, 24 fail):
- PASS: Clear (3), Lines (6), Rectangles (6), some Pies/Polygons/Composites
- FAIL: Ellipses (1-4%), Arcs (0.8-2.1%), AA lines (1.8-2.4%),
  Polygons/Pies/Composites with curves (0.5-3.6%)

Root causes of pixel differences:
- Ellipse/arc rasterization differs between Skia and GDI+
- Anti-aliasing algorithms produce different edge pixels
- Tolerances need tuning for curved geometry

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 1, 2026
…arios

- Always save 3 images per scenario: _actual, _reference, _diff
- CI uploads as 'diff-images' artifact for download
- Set TEST_ARTIFACTS_PATH env var in pixel_compat CI job
- continueOnError on test step so artifacts always upload

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 1, 2026
GDI+ treats integer coordinates with a +0.5 pixel center offset
for curve rasterization (ellipses, arcs, pies). Without this,
filled ellipses had 1-4% pixel error vs GDI+. With the fix,
error drops to ~0.08%.

Added GdiCurveRect() helper that applies +0.5 offset to all
curve bounding rects in DrawEllipse, FillEllipse, DrawArc,
DrawPie, FillPie.

Also added 17 boundary precision test scenarios:
- Even/odd ellipse sizes (4x4 through 80x80)
- Stroke widths 1-3px on ellipses
- Arc and pie at even/odd bounds
- Rect-vs-ellipse overlay comparison

328 tests pass, ApiCompat clean.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 1, 2026
Same GDI+ pixel-center offset (+0.5) that fixed ellipses also
applies to polygon vertices. Triangle stroke diff showed only
the left diagonal edge differing — exactly the pattern expected
from integer coordinate pixel-center vs pixel-edge addressing.

Added GdiPolygonPath() helper that shifts all vertices by +0.5.
Applied to DrawPolygon, FillPolygon, and FillPolygon(fillMode).

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 1, 2026
- Removed continueOnError: true — test failures now fail CI
- Added AA variants for Arcs (5), Pies (4), Polygons (6),
  Composites (3) — each curve scenario has both AA and non-AA
- AA categories (LinesAA, EllipsesAA, ArcsAA, PiesAA,
  PolygonsAA, CompositesAA) use 5% tolerance
- Non-AA categories use 0.5% tolerance
- 346 tests, all passing

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 2, 2026
Replaced 602-line DrawingScenarios.cs monolith with 16 category
files + ScenarioBase. Each class = category folder, each method =
scenario PNG. Adding a scenario is now: add a method, regenerate.

Structure:
  tools/SkiaSharp.Drawing.Scenarios/
  ├── ScenarioBase.cs       (base class + AllScenarios registry)
  ├── Clear.cs, Lines.cs, LinesAA.cs, Rectangles.cs
  ├── Ellipses.cs, EllipsesAA.cs, Arcs.cs, ArcsAA.cs
  ├── Pies.cs, PiesAA.cs, Polygons.cs, PolygonsAA.cs
  ├── Composites.cs, CompositesAA.cs, Colors.cs, Boundaries.cs

Also:
- Deleted redundant SelfConsistencyTests
- Updated .github/copilot-instructions.md with SkiaSharp.Drawing
  rules: implementation, test rules, tolerance tiers, how to add
  scenarios, build commands

340 tests pass, ApiCompat clean.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 2, 2026
Scenarios are now actual xUnit tests. No more custom runners.

  dotnet test tools/ReferenceGenerator/     → GDI+ PNGs (Windows CI)
  dotnet test tests/SkiaSharp.Drawing.Tests → runs 81 scenario tests
                                              + 259 unit tests
                                              + 81 reference comparisons

Changes:
- Added [Fact] to all 81 scenario methods across 16 category files
- ScenarioBase uses SCENARIO_OUTPUT_PATH env var (no constructor)
- Deleted SkiaRunner project (test project replaces it)
- ReferenceGenerator converted from console app to xUnit test project
- ReferenceComparisonTests discovers scenarios via directory scan +
  reflection instead of AllScenarios registry
- CI generate_references job uses dotnet test
- Updated copilot instructions

421 tests pass, 0 failures, ApiCompat clean.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 2, 2026
Replace SKBitmap + 64 SetPixel P/Invoke calls with:
- stackalloc uint[64] for the pixel buffer (256 bytes on stack)
- Single SKImage.FromPixelCopy bulk transfer
- No managed heap allocations for the tile

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
Patterns were vertically flipped — GDI+ anchors from row 0 (top),
our data had them at row 7 (bottom). Extracted correct bitmasks
by reading 8×8 pixel tiles from Windows CI GDI+ reference images.

Result: 48 hatch failures → 3 (only diagonal AA patterns remain).
Also cleaned orphan *Advanced/*Extended reference image dirs.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
SKColorF and color space testing confirmed the ±1 gradient rounding
is in the final float→byte quantization step, not the interpolation
math. SKColorF sRGB/linear produce identical byte output to SKColor.

Use SKPixelComparer.Compare(skia, gdi, tolerance: 3) which allows
|ΔR| + |ΔG| + |ΔB| ≤ 3 per pixel (±1 per channel). This correctly
passes gradient rounding while still catching real rendering bugs.

Result: 135 → 116 failures (19 gradient rounding tests now pass)

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
GDI+ only enables AA for SmoothingMode.AntiAlias and HighQuality.
Default, None, HighSpeed, and Invalid all render without AA.

Our code was enabling AA for everything except None and HighSpeed,
which caused scenarios without explicit SmoothingMode.None to
render with smooth edges while GDI+ rendered with aliased edges.

Fixed: ApplyState now only sets IsAntialias=true for AntiAlias
and HighQuality modes.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
Test shapes: triangle (3 colors), square (4 colors), pentagon (5),
star (10 vertices, single color), ellipse, off-center point,
single color, and L-shape (concave polygon).

All currently render as simple radial gradients using only the
first surround color — the per-vertex color interpolation is
not implemented.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 7, 2026
406 GDI + 406 Skia reference images. Local matches CI: 241/406 passing.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
Replace radial gradient approximation with SKCanvas.DrawVertices
using TriangleFan mode with per-vertex colors. This produces
gradients that follow the path shape with per-vertex color
interpolation.

- Flatten bezier/quadratic path segments to polyline vertices
- Build triangle fan: center (CenterColor) + boundary (SurroundColors)
- Clip to path for concave shapes
- Route FillPath, FillRectangle, FillEllipse through FillOnCanvas
  when brush is PathGradientBrush

Results: Triangle/Square/Pentagon/Star now render with correct
per-vertex color blending. Star went from 100% to 25% error.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
Skia represents ellipses/arcs as conic sections (rational quadratics),
not cubic beziers. Added SKPathVerb.Conic handling with weighted
subdivision to produce boundary points.

Ellipse PathGradient now renders correctly (yellow→blue) instead
of all-white. Error dropped from 50% to 35%.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
RotateFlipType encodes rotation in bits 0-1 and flip in bit 2.
The previous code compared enum member names but the enum has
overlapping aliases (e.g. RotateNoneFlipX=4=Rotate180FlipY),
so the flip flags were never set correctly.

Fixed by decoding the numeric value directly:
- Bits 0-1: rotation (0°, 90°, 180°, 270°)
- Bit 2: horizontal flip

For 90°/270° rotations, the flip axis must be swapped (X→Y)
because the coordinate system is rotated.

Result: 11 RotateFlip tests now pass (242 → 253 total passing).

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
12 files updated:
- Image.cs: RotationMask, FlipXMask, Rotation90/180/270, DefaultDpi
- Graphics.cs: GdiPixelCenterOffset, GdiTensionFactor, DefaultCurveTension,
  DefaultTextContrast, TextEmPaddingDivisor, CubicBezierStride
- HatchBrush.cs: HatchStyle.SolidDiamond for max index, tileSize, msbMask
- SkiaConversions.cs: DashLength, DotLength, GapLength for dash patterns
- Font.cs: PointsPerInch, DocumentUnitsPerInch, MillimetersPerInch
- GraphicsPath.cs: GdiTensionFactor, DefaultDpi, PointsPerInch
- GraphicsPathIterator.cs: PathPointType enum refs instead of hex literals
- PathGradientBrush.cs: QuadSubdivisions, CubicConicSubdivisions
- Bitmap.cs: BitsPerByte
- ColorTranslator.cs: channel masks and shifts
- PrinterUnitConvert.cs: unit conversion factors
- StandardPrintController.cs: PointsPerInch, HundredthsPerInch

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
406 GDI + 406 Skia. Local: 252/406 passing (62.1%).
RotateFlip fix confirmed: BitmapOperations 13 → 2 failures.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
Pen.Transform:
- Extract X scale from transform matrix, multiply stroke width

PenAlignment.Inset:
- Clip to shape boundary, double stroke width so only inner half shows
- Pen_Alignment_Inset: 16.7% → passing ✅

CompoundArray:
- Multi-stroke railroad track via GetFillPath + SKPathOp.Difference
- Pen_CompoundArray: 16.2% → 3.3%

StartCap:
- Use StartCap when equal to EndCap (was always using EndCap)

Refactored all Draw methods through central DrawStroke() helper
that handles normal, inset, and compound pen modes.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
Documented honestly:
- Pen.Transform: scale-only, no rotation/shear
- StartCap vs EndCap: single cap fallback when different
- Anchor caps: not implemented (RoundAnchor, DiamondAnchor, etc.)
- DashCap: stored but not applied
- PenAlignment.Inset: clip trick, not true inset
- CompoundArray: GetFillPath approximation
- PathGradientBrush: triangle fan (barycentric) vs GDI+ ray interpolation
- Blend/InterpolationColors on PathGradient: not applied to fan
- PixelOffsetMode.Half/HighQuality: stored, not applied
- CompositingMode.SourceCopy: stored, not applied
- Diagonal hatch AA: Skia no-AA vs GDI+ sub-pixel blending
- Gradient rounding: ±1 per channel, per-pixel tolerance of 3
- Cardinal spline: tension*0.3 (Wine) vs tension/3 (standard)
- SmoothingMode.Default: no AA (matching GDI+)
- DrawImage: boundary sampling differences

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
CompositingMode.SourceCopy:
- ApplyState now sets paint.BlendMode = Src for SourceCopy
- DrawImageCore also applies compositing mode
- Compositing_SourceCopy: 25% → passing ✅

PixelOffsetMode.Half/HighQuality:
- Property setter applies -0.5px canvas translate
- Restore() re-applies offset after canvas state restore
- PixelOffset_Half: 2.2% → passing ✅
- PixelOffset_HighQuality: 2.2% → passing ✅

SaveRestore clip:
- Save() creates clip scope so SetClip(Replace) doesn't leak past
- SaveRestore_Clip: 9% → passing ✅

Bitmap_GetSetPixel (63%):
- Investigated: bilinear interpolation sampling difference when
  upscaling checkerboard. Not fixable. Documented in KNOWN-LIMITATIONS.

Updated KNOWN-LIMITATIONS.md to reflect implemented features.

Co-authored-by: Copilot <[email protected]>
github-actions Bot pushed a commit that referenced this pull request May 8, 2026
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.

1 participant