Skip to content

Commit d2bfaf2

Browse files
committed
Merge branch 'master' into Issue-551-solidbrush-blend-performance
# Conflicts: # src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs
2 parents 0d81089 + 6431723 commit d2bfaf2

File tree

6 files changed

+273
-49
lines changed

6 files changed

+273
-49
lines changed

src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle source
9191
sourceRectangle,
9292
this.options))
9393
{
94-
amount.Span.Fill(this.options.BlendPercentage);
94+
amount.Span.Fill(1f);
9595

9696
Parallel.For(
9797
minY,
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Six Labors and contributors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
using SixLabors.ImageSharp.Advanced;
7+
using SixLabors.ImageSharp.Memory;
8+
using SixLabors.ImageSharp.PixelFormats;
9+
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
10+
using SixLabors.ImageSharp.Processing.Processors;
11+
using SixLabors.Primitives;
12+
13+
namespace SixLabors.ImageSharp.Processing.Drawing.Processors
14+
{
15+
/// <summary>
16+
/// Using the brush as a source of pixels colors blends the brush color with source.
17+
/// </summary>
18+
/// <typeparam name="TPixel">The pixel format.</typeparam>
19+
internal class FillProcessor<TPixel> : ImageProcessor<TPixel>
20+
where TPixel : struct, IPixel<TPixel>
21+
{
22+
/// <summary>
23+
/// The brush.
24+
/// </summary>
25+
private readonly IBrush<TPixel> brush;
26+
private readonly GraphicsOptions options;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
30+
/// </summary>
31+
/// <param name="brush">The brush to source pixel colors from.</param>
32+
/// <param name="options">The options</param>
33+
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options)
34+
{
35+
this.brush = brush;
36+
this.options = options;
37+
}
38+
39+
/// <inheritdoc/>
40+
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration)
41+
{
42+
int startX = sourceRectangle.X;
43+
int endX = sourceRectangle.Right;
44+
int startY = sourceRectangle.Y;
45+
int endY = sourceRectangle.Bottom;
46+
47+
// Align start/end positions.
48+
int minX = Math.Max(0, startX);
49+
int maxX = Math.Min(source.Width, endX);
50+
int minY = Math.Max(0, startY);
51+
int maxY = Math.Min(source.Height, endY);
52+
53+
int width = maxX - minX;
54+
55+
<<<<<<< HEAD
56+
var solidBrush = this.brush as SolidBrush<TPixel>;
57+
=======
58+
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width))
59+
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
60+
source,
61+
sourceRectangle,
62+
this.options))
63+
{
64+
amount.Span.Fill(1f);
65+
>>>>>>> master
66+
67+
// If there's no reason for blending, then avoid it.
68+
if (solidBrush != null &&
69+
(
70+
(this.options.BlenderMode == PixelBlenderMode.Normal && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f) ||
71+
(this.options.BlenderMode == PixelBlenderMode.Over && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f) ||
72+
(this.options.BlenderMode == PixelBlenderMode.Src)))
73+
{
74+
Parallel.For(
75+
minY,
76+
maxY,
77+
configuration.ParallelOptions,
78+
y =>
79+
{
80+
int offsetY = y - startY;
81+
int offsetX = minX - startX;
82+
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color);
83+
});
84+
}
85+
else
86+
{
87+
// Reset offset if necessary.
88+
if (minX > 0)
89+
{
90+
startX = 0;
91+
}
92+
93+
if (minY > 0)
94+
{
95+
startY = 0;
96+
}
97+
98+
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width))
99+
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator(
100+
source,
101+
sourceRectangle,
102+
this.options))
103+
{
104+
amount.Span.Fill(this.options.BlendPercentage);
105+
106+
Parallel.For(
107+
minY,
108+
maxY,
109+
configuration.ParallelOptions,
110+
y =>
111+
{
112+
int offsetY = y - startY;
113+
int offsetX = minX - startX;
114+
115+
applicator.Apply(amount.Span, offsetX, offsetY);
116+
});
117+
}
118+
}
119+
}
120+
}
121+
}
Lines changed: 129 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,164 @@
11
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

4-
using System.Numerics;
54
using SixLabors.ImageSharp.PixelFormats;
65
using SixLabors.ImageSharp.Processing;
76
using SixLabors.ImageSharp.Processing.Drawing;
8-
using SixLabors.ImageSharp.Processing.Overlays;
7+
using SixLabors.ImageSharp.Primitives;
8+
using SixLabors.ImageSharp.Processing.Drawing.Brushes;
9+
using SixLabors.Shapes;
910
using Xunit;
11+
// ReSharper disable InconsistentNaming
1012

1113
namespace SixLabors.ImageSharp.Tests.Drawing
1214
{
13-
public class FillSolidBrushTests : FileTestBase
15+
16+
17+
[GroupOutput("Drawing")]
18+
public class FillSolidBrushTests
1419
{
15-
[Fact]
16-
public void ImageShouldBeFloodFilledWithColorOnDefaultBackground()
20+
[Theory]
21+
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
22+
[WithBlankImages(7, 4, PixelTypes.Rgba32)]
23+
[WithBlankImages(16, 7, PixelTypes.Rgba32)]
24+
[WithBlankImages(33, 32, PixelTypes.Rgba32)]
25+
[WithBlankImages(400, 500, PixelTypes.Rgba32)]
26+
public void DoesNotDependOnSize<TPixel>(TestImageProvider<TPixel> provider)
27+
where TPixel : struct, IPixel<TPixel>
1728
{
18-
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush");
19-
using (var image = new Image<Rgba32>(500, 500))
29+
using (Image<TPixel> image = provider.GetImage())
2030
{
21-
image.Mutate(x => x.Fill(Rgba32.HotPink));
22-
image.Save($"{path}/DefaultBack.png");
23-
24-
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
25-
{
26-
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]);
31+
TPixel color = NamedColors<TPixel>.HotPink;
32+
image.Mutate(c => c.Fill(color));
2733

28-
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]);
29-
}
34+
image.DebugSave(provider, appendPixelTypeToFileName: false);
35+
image.ComparePixelBufferTo(color);
3036
}
3137
}
3238

33-
[Fact]
34-
public void ImageShouldBeFloodFilledWithColor()
39+
[Theory]
40+
[WithBlankImages(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)]
41+
public void DoesNotDependOnSinglePixelType<TPixel>(TestImageProvider<TPixel> provider)
42+
where TPixel : struct, IPixel<TPixel>
3543
{
36-
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush");
37-
using (var image = new Image<Rgba32>(500, 500))
44+
using (Image<TPixel> image = provider.GetImage())
3845
{
39-
image.Mutate(x => x
40-
.BackgroundColor(Rgba32.Blue)
41-
.Fill(Rgba32.HotPink));
42-
image.Save($"{path}/Simple.png");
46+
TPixel color = NamedColors<TPixel>.HotPink;
47+
image.Mutate(c => c.Fill(color));
4348

44-
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
45-
{
46-
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]);
47-
48-
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]);
49-
}
49+
image.DebugSave(provider, appendSourceFileOrDescription: false);
50+
image.ComparePixelBufferTo(color);
5051
}
5152
}
5253

53-
[Fact]
54-
public void ImageShouldBeFloodFilledWithColorOpacity()
54+
[Theory]
55+
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")]
56+
[WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")]
57+
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(TestImageProvider<TPixel> provider, string newColorName)
58+
where TPixel : struct, IPixel<TPixel>
5559
{
56-
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush");
57-
using (var image = new Image<Rgba32>(500, 500))
60+
using (Image<TPixel> image = provider.GetImage())
5861
{
59-
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150);
62+
TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName);
63+
image.Mutate(c => c.Fill(color));
64+
65+
image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false);
66+
image.ComparePixelBufferTo(color);
67+
}
68+
}
69+
70+
public static readonly TheoryData<bool, string, float, PixelBlenderMode, float> BlendData =
71+
new TheoryData<bool, string, float, PixelBlenderMode, float>()
72+
{
73+
{ false, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f },
74+
{ false, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f },
75+
{ false, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f },
76+
{ false, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f },
77+
78+
{ false, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f },
79+
{ false, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f },
80+
{ false, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f },
81+
{ false, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f },
82+
83+
{ false, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f },
84+
{ false, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f },
85+
{ false, "Green", 0.5f, PixelBlenderMode.Add, 0.3f },
86+
{ false, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f },
6087

61-
image.Mutate(x => x
62-
.BackgroundColor(Rgba32.Blue)
63-
.Fill(color));
64-
image.Save($"{path}/Opacity.png");
88+
{ true, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f },
89+
{ true, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f },
90+
{ true, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f },
91+
{ true, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f },
6592

66-
//shift background color towards forground color by the opacity amount
67-
var mergedColor = new Rgba32(Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f));
93+
{ true, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f },
94+
{ true, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f },
95+
{ true, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f },
96+
{ true, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f },
6897

98+
{ true, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f },
99+
{ true, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f },
100+
{ true, "Green", 0.5f, PixelBlenderMode.Add, 0.3f },
101+
{ true, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f },
102+
};
69103

70-
using (PixelAccessor<Rgba32> sourcePixels = image.Lock())
104+
[Theory]
105+
[WithSolidFilledImages(nameof(BlendData), 16, 16, "Red", PixelTypes.Rgba32)]
106+
public void BlendFillColorOverBackround<TPixel>(
107+
TestImageProvider<TPixel> provider,
108+
bool triggerFillRegion,
109+
string newColorName,
110+
float alpha,
111+
PixelBlenderMode blenderMode,
112+
float blendPercentage)
113+
where TPixel : struct, IPixel<TPixel>
114+
{
115+
var vec = TestUtils.GetPixelOfNamedColor<RgbaVector>(newColorName).ToVector4();
116+
vec.W = alpha;
117+
118+
TPixel fillColor = default;
119+
fillColor.PackFromVector4(vec);
120+
121+
using (Image<TPixel> image = provider.GetImage())
122+
{
123+
TPixel bgColor = image[0, 0];
124+
125+
var options = new GraphicsOptions(false)
126+
{
127+
BlenderMode = blenderMode,
128+
BlendPercentage = blendPercentage
129+
};
130+
131+
if (triggerFillRegion)
132+
{
133+
var region = new ShapeRegion(new RectangularPolygon(0, 0, 16, 16));
134+
135+
image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor), region));
136+
}
137+
else
71138
{
72-
Assert.Equal(mergedColor, sourcePixels[9, 9]);
73-
Assert.Equal(mergedColor, sourcePixels[199, 149]);
139+
image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor)));
74140
}
141+
142+
var testOutputDetails = new
143+
{
144+
triggerFillRegion = triggerFillRegion,
145+
newColorName = newColorName,
146+
alpha = alpha,
147+
blenderMode = blenderMode,
148+
blendPercentage = blendPercentage
149+
};
150+
151+
image.DebugSave(
152+
provider,
153+
testOutputDetails,
154+
appendPixelTypeToFileName: false,
155+
appendSourceFileOrDescription: false);
156+
157+
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(blenderMode);
158+
TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage);
159+
160+
image.ComparePixelBufferTo(expectedPixel);
75161
}
76162
}
77-
78163
}
79164
}

tests/ImageSharp.Tests/TestUtilities/Attributes/WithSolidFilledImagesAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public WithSolidFilledImagesAttribute(
133133
{
134134
Guard.NotNull(colorName, nameof(colorName));
135135

136-
var c = (Rgba32)typeof(Rgba32).GetTypeInfo().GetField(colorName).GetValue(null);
136+
Rgba32 c = TestUtils.GetPixelOfNamedColor<Rgba32>(colorName);
137137
this.R = c.R;
138138
this.G = c.G;
139139
this.B = c.B;

tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -346,13 +346,26 @@ public static Image<TPixel> ComparePixelBufferTo<TPixel>(
346346
Span<TPixel> expectedPixels)
347347
where TPixel : struct, IPixel<TPixel>
348348
{
349-
Span<TPixel> actual = image.GetPixelSpan();
349+
Span<TPixel> actualPixels = image.GetPixelSpan();
350350

351-
Assert.True(expectedPixels.Length == actual.Length, "Buffer sizes are not equal!");
351+
Assert.True(expectedPixels.Length == actualPixels.Length, "Buffer sizes are not equal!");
352352

353353
for (int i = 0; i < expectedPixels.Length; i++)
354354
{
355-
Assert.True(expectedPixels[i].Equals(actual[i]), $"Pixels are different on position {i}!");
355+
Assert.True(expectedPixels[i].Equals(actualPixels[i]), $"Pixels are different on position {i}!");
356+
}
357+
358+
return image;
359+
}
360+
361+
public static Image<TPixel> ComparePixelBufferTo<TPixel>(this Image<TPixel> image, TPixel expectedPixel)
362+
where TPixel : struct, IPixel<TPixel>
363+
{
364+
Span<TPixel> actualPixels = image.GetPixelSpan();
365+
366+
for (int i = 0; i < actualPixels.Length; i++)
367+
{
368+
Assert.True(expectedPixel.Equals(actualPixels[i]), $"Pixels are different on position {i}!");
356369
}
357370

358371
return image;

tests/ImageSharp.Tests/TestUtilities/TestUtils.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ internal static bool HasAll(this PixelTypes pixelTypes, PixelTypes flagsToCheck)
147147
/// <returns>The pixel types</returns>
148148
internal static PixelTypes[] GetAllPixelTypes() => (PixelTypes[])Enum.GetValues(typeof(PixelTypes));
149149

150+
internal static TPixel GetPixelOfNamedColor<TPixel>(string colorName)
151+
where TPixel : struct, IPixel<TPixel>
152+
{
153+
return (TPixel)typeof(NamedColors<TPixel>).GetTypeInfo().GetField(colorName).GetValue(null);
154+
}
150155

151156
/// <summary>
152157
/// Utility for testing image processor extension methods:

0 commit comments

Comments
 (0)