Skip to content

Commit b6bc3f8

Browse files
Make DrawingCanvas factory internal and update tests
1 parent 9a12335 commit b6bc3f8

7 files changed

Lines changed: 47 additions & 345 deletions

File tree

src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ Before looking at the flow, it helps to define the major terms in the sense used
5959

6060
It is not the rasterizer. It is the object that makes the public drawing model coherent.
6161

62-
Most callers reach that model through `IImageProcessingContext.Paint(...)`, while lower-level code can
63-
also create a canvas directly with the `CreateCanvas(...)` extension methods.
62+
Callers reach that model through `IImageProcessingContext.Paint(...)`.
6463

6564
`DrawingCanvas<TPixel>` is the typed implementation that carries the target pixel format for brush normalization,
6665
readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same
@@ -107,9 +106,8 @@ There are two backend-selection paths in the architecture:
107106
- direct `DrawingCanvas<TPixel>` construction resolves the backend from `Configuration`
108107
- specialized infrastructure can construct a canvas with an explicit backend
109108

110-
The ordinary CPU entry points include `Paint(...)` on `IImageProcessingContext` and the `CreateCanvas(...)`
111-
extension methods on `Image`, `Image<TPixel>`, and `ImageFrame<TPixel>`. Those factories return `DrawingCanvas`
112-
and route into the typed implementation internally.
109+
The ordinary CPU entry point is `Paint(...)` on `IImageProcessingContext`, which routes into the typed
110+
implementation internally.
113111

114112
That explicit-backend path matters for the WebGPU helpers. `WebGPUWindow<TPixel>`, `WebGPURenderTarget<TPixel>`, and `WebGPUDeviceContext<TPixel>` create canvases that point directly at their owned `WebGPUDrawingBackend` instance instead of storing that backend on the caller's `Configuration`.
115113

Lines changed: 2 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,15 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4-
using System.Diagnostics.CodeAnalysis;
5-
using SixLabors.ImageSharp.Advanced;
64
using SixLabors.ImageSharp.Memory;
75

86
namespace SixLabors.ImageSharp.Drawing.Processing;
97

108
/// <summary>
119
/// Convenience extension methods for creating drawing canvas instances from ImageSharp image types.
1210
/// </summary>
13-
public static class DrawingCanvasFactoryExtensions
11+
internal static class DrawingCanvasFactoryExtensions
1412
{
15-
/// <summary>
16-
/// Creates a drawing canvas over a specific frame of an image whose pixel type is known only at runtime.
17-
/// </summary>
18-
/// <param name="image">The image containing the frame.</param>
19-
/// <param name="options">Initial drawing options for this canvas instance.</param>
20-
/// <param name="frameIndex">The zero-based frame index to target.</param>
21-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
22-
/// <returns>A drawing canvas targeting the selected frame.</returns>
23-
public static DrawingCanvas CreateCanvas(
24-
this Image image,
25-
DrawingOptions options,
26-
int frameIndex,
27-
params IPath[] clipPaths)
28-
=> CreateCanvas(image, image.Configuration, options, frameIndex, clipPaths);
29-
30-
/// <summary>
31-
/// Creates a drawing canvas over a specific frame of an image whose pixel type is known only at runtime.
32-
/// </summary>
33-
/// <param name="image">The image containing the frame.</param>
34-
/// <param name="configuration">The configuration to use for this canvas instance.</param>
35-
/// <param name="options">Initial drawing options for this canvas instance.</param>
36-
/// <param name="frameIndex">The zero-based frame index to target.</param>
37-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
38-
/// <returns>A drawing canvas targeting the selected frame.</returns>
39-
public static DrawingCanvas CreateCanvas(
40-
this Image image,
41-
Configuration configuration,
42-
DrawingOptions options,
43-
int frameIndex,
44-
params IPath[] clipPaths)
45-
{
46-
Guard.NotNull(image, nameof(image));
47-
Guard.NotNull(options, nameof(options));
48-
Guard.NotNull(clipPaths, nameof(clipPaths));
49-
Guard.MustBeBetweenOrEqualTo(frameIndex, 0, image.Frames.Count - 1, nameof(frameIndex));
50-
51-
CreateCanvasVisitor visitor = new(configuration, options, frameIndex, clipPaths);
52-
image.AcceptVisitor(visitor);
53-
return visitor.Canvas!;
54-
}
55-
56-
/// <summary>
57-
/// Creates a drawing canvas over the root frame of an image whose pixel type is known only at runtime.
58-
/// </summary>
59-
/// <param name="image">The image whose root frame should be targeted.</param>
60-
/// <param name="options">Initial drawing options for this canvas instance.</param>
61-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
62-
/// <returns>A drawing canvas targeting the root frame.</returns>
63-
public static DrawingCanvas CreateCanvas(
64-
this Image image,
65-
DrawingOptions options,
66-
params IPath[] clipPaths)
67-
=> CreateCanvas(image, image.Configuration, options, clipPaths);
68-
69-
/// <summary>
70-
/// Creates a drawing canvas over the root frame of an image whose pixel type is known only at runtime.
71-
/// </summary>
72-
/// <param name="image">The image whose root frame should be targeted.</param>
73-
/// <param name="configuration">The configuration to use for this canvas instance.</param>
74-
/// <param name="options">Initial drawing options for this canvas instance.</param>
75-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
76-
/// <returns>A drawing canvas targeting the root frame.</returns>
77-
public static DrawingCanvas CreateCanvas(
78-
this Image image,
79-
Configuration configuration,
80-
DrawingOptions options,
81-
params IPath[] clipPaths)
82-
{
83-
Guard.NotNull(image, nameof(image));
84-
Guard.NotNull(options, nameof(options));
85-
Guard.NotNull(clipPaths, nameof(clipPaths));
86-
87-
CreateCanvasVisitor visitor = new(configuration, options, frameIndex: 0, clipPaths);
88-
image.AcceptVisitor(visitor);
89-
return visitor.Canvas!;
90-
}
91-
92-
/// <summary>
93-
/// Creates a drawing canvas over an existing frame.
94-
/// </summary>
95-
/// <typeparam name="TPixel">The pixel format.</typeparam>
96-
/// <param name="frame">The frame backing the canvas.</param>
97-
/// <param name="options">Initial drawing options for this canvas instance.</param>
98-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
99-
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
100-
public static DrawingCanvas CreateCanvas<TPixel>(
101-
this ImageFrame<TPixel> frame,
102-
DrawingOptions options,
103-
params IPath[] clipPaths)
104-
where TPixel : unmanaged, IPixel<TPixel>
105-
=> CreateCanvas(frame, frame.Configuration, options, clipPaths);
106-
10713
/// <summary>
10814
/// Creates a drawing canvas over an existing frame.
10915
/// </summary>
@@ -113,7 +19,7 @@ public static DrawingCanvas CreateCanvas<TPixel>(
11319
/// <param name="options">Initial drawing options for this canvas instance.</param>
11420
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
11521
/// <returns>A drawing canvas targeting <paramref name="frame"/>.</returns>
116-
public static DrawingCanvas CreateCanvas<TPixel>(
22+
internal static DrawingCanvas CreateCanvas<TPixel>(
11723
this ImageFrame<TPixel> frame,
11824
Configuration configuration,
11925
DrawingOptions options,
@@ -130,116 +36,4 @@ public static DrawingCanvas CreateCanvas<TPixel>(
13036
new Buffer2DRegion<TPixel>(frame.PixelBuffer),
13137
clipPaths);
13238
}
133-
134-
/// <summary>
135-
/// Creates a drawing canvas over a specific frame of an image.
136-
/// </summary>
137-
/// <typeparam name="TPixel">The pixel format.</typeparam>
138-
/// <param name="image">The image containing the frame.</param>
139-
/// <param name="options">Initial drawing options for this canvas instance.</param>
140-
/// <param name="frameIndex">The zero-based frame index to target.</param>
141-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
142-
/// <returns>A drawing canvas targeting the selected frame.</returns>
143-
public static DrawingCanvas CreateCanvas<TPixel>(
144-
this Image<TPixel> image,
145-
DrawingOptions options,
146-
int frameIndex,
147-
params IPath[] clipPaths)
148-
where TPixel : unmanaged, IPixel<TPixel>
149-
=> CreateCanvas(image, image.Configuration, options, frameIndex, clipPaths);
150-
151-
/// <summary>
152-
/// Creates a drawing canvas over a specific frame of an image.
153-
/// </summary>
154-
/// <typeparam name="TPixel">The pixel format.</typeparam>
155-
/// <param name="image">The image containing the frame.</param>
156-
/// <param name="configuration">The configuration to use for this canvas instance.</param>
157-
/// <param name="options">Initial drawing options for this canvas instance.</param>
158-
/// <param name="frameIndex">The zero-based frame index to target.</param>
159-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
160-
/// <returns>A drawing canvas targeting the selected frame.</returns>
161-
public static DrawingCanvas CreateCanvas<TPixel>(
162-
this Image<TPixel> image,
163-
Configuration configuration,
164-
DrawingOptions options,
165-
int frameIndex,
166-
params IPath[] clipPaths)
167-
where TPixel : unmanaged, IPixel<TPixel>
168-
{
169-
Guard.NotNull(image, nameof(image));
170-
Guard.NotNull(options, nameof(options));
171-
Guard.NotNull(clipPaths, nameof(clipPaths));
172-
Guard.MustBeBetweenOrEqualTo(frameIndex, 0, image.Frames.Count - 1, nameof(frameIndex));
173-
174-
return image.Frames[frameIndex].CreateCanvas(configuration, options, clipPaths);
175-
}
176-
177-
/// <summary>
178-
/// Creates a drawing canvas over the root frame of an image.
179-
/// </summary>
180-
/// <typeparam name="TPixel">The pixel format.</typeparam>
181-
/// <param name="image">The image whose root frame should be targeted.</param>
182-
/// <param name="options">Initial drawing options for this canvas instance.</param>
183-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
184-
/// <returns>A drawing canvas targeting the root frame.</returns>
185-
public static DrawingCanvas CreateCanvas<TPixel>(
186-
this Image<TPixel> image,
187-
DrawingOptions options,
188-
params IPath[] clipPaths)
189-
where TPixel : unmanaged, IPixel<TPixel>
190-
=> CreateCanvas(image, image.Configuration, options, clipPaths);
191-
192-
/// <summary>
193-
/// Creates a drawing canvas over the root frame of an image.
194-
/// </summary>
195-
/// <typeparam name="TPixel">The pixel format.</typeparam>
196-
/// <param name="image">The image whose root frame should be targeted.</param>
197-
/// <param name="configuration">The configuration to use for this canvas instance.</param>
198-
/// <param name="options">Initial drawing options for this canvas instance.</param>
199-
/// <param name="clipPaths">Initial clip paths for this canvas instance.</param>
200-
/// <returns>A drawing canvas targeting the root frame.</returns>
201-
public static DrawingCanvas CreateCanvas<TPixel>(
202-
this Image<TPixel> image,
203-
Configuration configuration,
204-
DrawingOptions options,
205-
params IPath[] clipPaths)
206-
where TPixel : unmanaged, IPixel<TPixel>
207-
{
208-
Guard.NotNull(image, nameof(image));
209-
Guard.NotNull(options, nameof(options));
210-
Guard.NotNull(clipPaths, nameof(clipPaths));
211-
212-
return image.Frames.RootFrame.CreateCanvas(configuration, options, clipPaths);
213-
}
214-
215-
private sealed class CreateCanvasVisitor : IImageVisitor
216-
{
217-
private readonly Configuration configuration;
218-
private readonly DrawingOptions options;
219-
private readonly int frameIndex;
220-
private readonly IPath[] clipPaths;
221-
222-
public CreateCanvasVisitor(
223-
Configuration configuration,
224-
DrawingOptions options,
225-
int frameIndex,
226-
IPath[] clipPaths)
227-
{
228-
this.configuration = configuration;
229-
this.options = options;
230-
this.frameIndex = frameIndex;
231-
this.clipPaths = clipPaths;
232-
}
233-
234-
public DrawingCanvas? Canvas { get; private set; }
235-
236-
[MemberNotNull(nameof(Canvas))]
237-
public void Visit<TPixel>(Image<TPixel> image)
238-
where TPixel : unmanaged, IPixel<TPixel>
239-
=> this.Canvas = image.CreateCanvas(
240-
this.configuration,
241-
this.options,
242-
this.frameIndex,
243-
this.clipPaths);
244-
}
24539
}

tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextRepeatedGlyphs.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using SixLabors.ImageSharp.Drawing.Processing;
77
using SixLabors.ImageSharp.Drawing.Processing.Backends;
88
using SixLabors.ImageSharp.PixelFormats;
9+
using SixLabors.ImageSharp.Processing;
910

1011
namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing;
1112

@@ -68,9 +69,9 @@ public void Cleanup()
6869
[Benchmark(Baseline = true, Description = "DrawingCanvas Default Backend")]
6970
public void DrawingCanvasDefaultBackend()
7071
{
71-
using DrawingCanvas canvas = this.defaultImage.CreateCanvas(this.drawingOptions);
72-
canvas.DrawText(this.textOptions, this.text, this.brush, null);
73-
canvas.Flush();
72+
this.defaultImage.Mutate(c => c.Paint(
73+
this.drawingOptions,
74+
canvas => canvas.DrawText(this.textOptions, this.text, this.brush, null)));
7475
}
7576

7677
[Benchmark(Description = "DrawingCanvas WebGPU Backend (NativeSurface)")]

tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -619,36 +619,26 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath<TPixel>(Tes
619619
string text = new('A', glyphCount);
620620
Brush drawBrush = Brushes.Solid(Color.HotPink);
621621
Brush clearBrush = Brushes.Solid(Color.White);
622-
using Image<TPixel> defaultImage = provider.GetImage();
623-
using (DrawingCanvas defaultClearCanvas = defaultImage.CreateCanvas(Configuration.Default, clearOptions))
622+
void DrawAction(DrawingCanvas canvas)
624623
{
625-
defaultClearCanvas.Fill(clearBrush);
626-
defaultClearCanvas.Flush();
624+
canvas.Fill(clearBrush);
625+
canvas.Flush();
626+
canvas.Save(drawingOptions);
627+
canvas.DrawText(textOptions, text, drawBrush, null);
628+
canvas.Restore();
627629
}
628630

629-
using (DrawingCanvas defaultDrawCanvas = defaultImage.CreateCanvas(Configuration.Default, drawingOptions))
630-
{
631-
defaultDrawCanvas.DrawText(textOptions, text, drawBrush, null);
632-
defaultDrawCanvas.Flush();
633-
}
631+
using Image<TPixel> defaultImage = provider.GetImage();
632+
defaultImage.Mutate(c => c.Paint(clearOptions, DrawAction));
634633

635634
using WebGPUDrawingBackend nativeSurfaceBackend = new();
636635
using WebGPURenderTarget<TPixel> renderTarget = new(defaultImage.Width, defaultImage.Height);
637636
Configuration nativeSurfaceConfiguration = Configuration.Default.Clone();
638637
nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend);
639638

640-
using (DrawingCanvas<TPixel> nativeSurfaceClearCanvas =
641-
new(nativeSurfaceConfiguration, clearOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
639+
using (DrawingCanvas<TPixel> nativeSurfaceCanvas = new(nativeSurfaceConfiguration, clearOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
642640
{
643-
nativeSurfaceClearCanvas.Fill(clearBrush);
644-
nativeSurfaceClearCanvas.Flush();
645-
}
646-
647-
using (DrawingCanvas<TPixel> nativeSurfaceDrawCanvas =
648-
new(nativeSurfaceConfiguration, drawingOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
649-
{
650-
nativeSurfaceDrawCanvas.DrawText(textOptions, text, drawBrush, null);
651-
nativeSurfaceDrawCanvas.Flush();
641+
DrawAction(nativeSurfaceCanvas);
652642
}
653643

654644
using Image<TPixel> nativeSurfaceImage = renderTarget.Readback();
@@ -657,13 +647,8 @@ public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath<TPixel>(Tes
657647
AssertBackendPairReferenceOutputs(provider, "RepeatedGlyphs_AfterClear", defaultImage, nativeSurfaceImage);
658648
}
659649

660-
private static void RenderWithDefaultBackend<TPixel>(Image<TPixel> image, DrawingOptions options, Action<DrawingCanvas> drawAction)
661-
where TPixel : unmanaged, IPixel<TPixel>
662-
{
663-
using DrawingCanvas canvas = image.CreateCanvas(Configuration.Default, options);
664-
drawAction(canvas);
665-
canvas.Flush();
666-
}
650+
private static void RenderWithDefaultBackend<TPixel>(Image<TPixel> image, DrawingOptions options, CanvasAction drawAction)
651+
where TPixel : unmanaged, IPixel<TPixel> => image.Mutate(c => c.Paint(options, drawAction));
667652

668653
private static IPath CreateLargeSceneDenseRectangleGridPath()
669654
{
@@ -1307,40 +1292,25 @@ public void MultipleFlushes_OnSameBackend_ProduceCorrectResults<TPixel>(TestImag
13071292
Brush blueBrush = Brushes.Solid(Color.Blue);
13081293
RectangularPolygon rect1 = new(20, 20, 120, 80);
13091294
RectangularPolygon rect2 = new(160, 100, 120, 80);
1310-
1311-
// Default backend: two separate flushes.
1312-
using Image<TPixel> defaultImage = provider.GetImage();
1313-
using (DrawingCanvas canvas1 = defaultImage.CreateCanvas(Configuration.Default, drawingOptions))
1295+
void DrawAction(DrawingCanvas canvas)
13141296
{
1315-
canvas1.Fill(redBrush, rect1);
1316-
canvas1.Flush();
1297+
canvas.Clear(Brushes.Solid(Color.White));
1298+
canvas.Fill(redBrush, rect1);
1299+
canvas.Flush();
1300+
canvas.Fill(blueBrush, rect2);
13171301
}
13181302

1319-
using (DrawingCanvas canvas2 = defaultImage.CreateCanvas(Configuration.Default, drawingOptions))
1320-
{
1321-
canvas2.Fill(blueBrush, rect2);
1322-
canvas2.Flush();
1323-
}
1303+
using Image<TPixel> defaultImage = provider.GetImage();
1304+
defaultImage.Mutate(c => c.Paint(drawingOptions, DrawAction));
13241305

1325-
// Native surface: two separate flushes reusing same backend.
13261306
using WebGPUDrawingBackend nativeSurfaceBackend = new();
13271307
using WebGPURenderTarget<TPixel> renderTarget = new(defaultImage.Width, defaultImage.Height);
13281308
Configuration nativeConfig = Configuration.Default.Clone();
13291309
nativeConfig.SetDrawingBackend(nativeSurfaceBackend);
13301310

1331-
using (DrawingCanvas<TPixel> canvas1 =
1332-
new(nativeConfig, drawingOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
1333-
{
1334-
canvas1.Clear(Brushes.Solid(Color.White));
1335-
canvas1.Fill(redBrush, rect1);
1336-
canvas1.Flush();
1337-
}
1338-
1339-
using (DrawingCanvas<TPixel> canvas2 =
1340-
new(nativeConfig, drawingOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
1311+
using (DrawingCanvas<TPixel> canvas = new(nativeConfig, drawingOptions, nativeSurfaceBackend, renderTarget.NativeFrame))
13411312
{
1342-
canvas2.Fill(blueBrush, rect2);
1343-
canvas2.Flush();
1313+
DrawAction(canvas);
13441314
}
13451315

13461316
using Image<TPixel> nativeSurfaceImage = renderTarget.Readback();

0 commit comments

Comments
 (0)