mirror of https://github.com/SixLabors/ImageSharp
Browse Source
# Conflicts: # src/ImageSharp.Drawing/Processing/Drawing/Processors/FillProcessor.cspull/552/head
6 changed files with 273 additions and 49 deletions
@ -0,0 +1,121 @@ |
|||||
|
// Copyright (c) Six Labors and contributors.
|
||||
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using SixLabors.ImageSharp.Advanced; |
||||
|
using SixLabors.ImageSharp.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing.Brushes; |
||||
|
using SixLabors.ImageSharp.Processing.Processors; |
||||
|
using SixLabors.Primitives; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Processing.Drawing.Processors |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Using the brush as a source of pixels colors blends the brush color with source.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
||||
|
internal class FillProcessor<TPixel> : ImageProcessor<TPixel> |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The brush.
|
||||
|
/// </summary>
|
||||
|
private readonly IBrush<TPixel> brush; |
||||
|
private readonly GraphicsOptions options; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="FillProcessor{TPixel}"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="brush">The brush to source pixel colors from.</param>
|
||||
|
/// <param name="options">The options</param>
|
||||
|
public FillProcessor(IBrush<TPixel> brush, GraphicsOptions options) |
||||
|
{ |
||||
|
this.brush = brush; |
||||
|
this.options = options; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnFrameApply(ImageFrame<TPixel> source, Rectangle sourceRectangle, Configuration configuration) |
||||
|
{ |
||||
|
int startX = sourceRectangle.X; |
||||
|
int endX = sourceRectangle.Right; |
||||
|
int startY = sourceRectangle.Y; |
||||
|
int endY = sourceRectangle.Bottom; |
||||
|
|
||||
|
// Align start/end positions.
|
||||
|
int minX = Math.Max(0, startX); |
||||
|
int maxX = Math.Min(source.Width, endX); |
||||
|
int minY = Math.Max(0, startY); |
||||
|
int maxY = Math.Min(source.Height, endY); |
||||
|
|
||||
|
int width = maxX - minX; |
||||
|
|
||||
|
<<<<<<< HEAD |
||||
|
var solidBrush = this.brush as SolidBrush<TPixel>; |
||||
|
======= |
||||
|
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width)) |
||||
|
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator( |
||||
|
source, |
||||
|
sourceRectangle, |
||||
|
this.options)) |
||||
|
{ |
||||
|
amount.Span.Fill(1f); |
||||
|
>>>>>>> master |
||||
|
|
||||
|
// If there's no reason for blending, then avoid it.
|
||||
|
if (solidBrush != null && |
||||
|
( |
||||
|
(this.options.BlenderMode == PixelBlenderMode.Normal && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f) || |
||||
|
(this.options.BlenderMode == PixelBlenderMode.Over && this.options.BlendPercentage == 1f && solidBrush.Color.ToVector4().W == 1f) || |
||||
|
(this.options.BlenderMode == PixelBlenderMode.Src))) |
||||
|
{ |
||||
|
Parallel.For( |
||||
|
minY, |
||||
|
maxY, |
||||
|
configuration.ParallelOptions, |
||||
|
y => |
||||
|
{ |
||||
|
int offsetY = y - startY; |
||||
|
int offsetX = minX - startX; |
||||
|
source.GetPixelRowSpan(y).Slice(minX, width).Fill(solidBrush.Color); |
||||
|
}); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Reset offset if necessary.
|
||||
|
if (minX > 0) |
||||
|
{ |
||||
|
startX = 0; |
||||
|
} |
||||
|
|
||||
|
if (minY > 0) |
||||
|
{ |
||||
|
startY = 0; |
||||
|
} |
||||
|
|
||||
|
using (IBuffer<float> amount = source.MemoryManager.Allocate<float>(width)) |
||||
|
using (BrushApplicator<TPixel> applicator = this.brush.CreateApplicator( |
||||
|
source, |
||||
|
sourceRectangle, |
||||
|
this.options)) |
||||
|
{ |
||||
|
amount.Span.Fill(this.options.BlendPercentage); |
||||
|
|
||||
|
Parallel.For( |
||||
|
minY, |
||||
|
maxY, |
||||
|
configuration.ParallelOptions, |
||||
|
y => |
||||
|
{ |
||||
|
int offsetY = y - startY; |
||||
|
int offsetX = minX - startX; |
||||
|
|
||||
|
applicator.Apply(amount.Span, offsetX, offsetY); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,79 +1,164 @@ |
|||||
// Copyright (c) Six Labors and contributors.
|
// Copyright (c) Six Labors and contributors.
|
||||
// Licensed under the Apache License, Version 2.0.
|
// Licensed under the Apache License, Version 2.0.
|
||||
|
|
||||
using System.Numerics; |
|
||||
using SixLabors.ImageSharp.PixelFormats; |
using SixLabors.ImageSharp.PixelFormats; |
||||
using SixLabors.ImageSharp.Processing; |
using SixLabors.ImageSharp.Processing; |
||||
using SixLabors.ImageSharp.Processing.Drawing; |
using SixLabors.ImageSharp.Processing.Drawing; |
||||
using SixLabors.ImageSharp.Processing.Overlays; |
using SixLabors.ImageSharp.Primitives; |
||||
|
using SixLabors.ImageSharp.Processing.Drawing.Brushes; |
||||
|
using SixLabors.Shapes; |
||||
using Xunit; |
using Xunit; |
||||
|
// ReSharper disable InconsistentNaming
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Drawing |
namespace SixLabors.ImageSharp.Tests.Drawing |
||||
{ |
{ |
||||
public class FillSolidBrushTests : FileTestBase |
|
||||
|
|
||||
|
[GroupOutput("Drawing")] |
||||
|
public class FillSolidBrushTests |
||||
{ |
{ |
||||
[Fact] |
[Theory] |
||||
public void ImageShouldBeFloodFilledWithColorOnDefaultBackground() |
[WithBlankImages(1, 1, PixelTypes.Rgba32)] |
||||
|
[WithBlankImages(7, 4, PixelTypes.Rgba32)] |
||||
|
[WithBlankImages(16, 7, PixelTypes.Rgba32)] |
||||
|
[WithBlankImages(33, 32, PixelTypes.Rgba32)] |
||||
|
[WithBlankImages(400, 500, PixelTypes.Rgba32)] |
||||
|
public void DoesNotDependOnSize<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); |
using (Image<TPixel> image = provider.GetImage()) |
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
{ |
||||
image.Mutate(x => x.Fill(Rgba32.HotPink)); |
TPixel color = NamedColors<TPixel>.HotPink; |
||||
image.Save($"{path}/DefaultBack.png"); |
image.Mutate(c => c.Fill(color)); |
||||
|
|
||||
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) |
|
||||
{ |
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
image.DebugSave(provider, appendPixelTypeToFileName: false); |
||||
} |
image.ComparePixelBufferTo(color); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
[Fact] |
[Theory] |
||||
public void ImageShouldBeFloodFilledWithColor() |
[WithBlankImages(16, 16, PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.RgbaVector)] |
||||
|
public void DoesNotDependOnSinglePixelType<TPixel>(TestImageProvider<TPixel> provider) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); |
using (Image<TPixel> image = provider.GetImage()) |
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
{ |
||||
image.Mutate(x => x |
TPixel color = NamedColors<TPixel>.HotPink; |
||||
.BackgroundColor(Rgba32.Blue) |
image.Mutate(c => c.Fill(color)); |
||||
.Fill(Rgba32.HotPink)); |
|
||||
image.Save($"{path}/Simple.png"); |
|
||||
|
|
||||
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) |
image.DebugSave(provider, appendSourceFileOrDescription: false); |
||||
{ |
image.ComparePixelBufferTo(color); |
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[9, 9]); |
|
||||
|
|
||||
Assert.Equal(Rgba32.HotPink, sourcePixels[199, 149]); |
|
||||
} |
|
||||
} |
} |
||||
} |
} |
||||
|
|
||||
[Fact] |
[Theory] |
||||
public void ImageShouldBeFloodFilledWithColorOpacity() |
[WithSolidFilledImages(16, 16, "Red", PixelTypes.Rgba32, "Blue")] |
||||
|
[WithSolidFilledImages(16, 16, "Yellow", PixelTypes.Rgba32, "Khaki")] |
||||
|
public void WhenColorIsOpaque_OverridePreviousColor<TPixel>(TestImageProvider<TPixel> provider, string newColorName) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
{ |
{ |
||||
string path = TestEnvironment.CreateOutputDirectory("Fill", "SolidBrush"); |
using (Image<TPixel> image = provider.GetImage()) |
||||
using (var image = new Image<Rgba32>(500, 500)) |
|
||||
{ |
{ |
||||
var color = new Rgba32(Rgba32.HotPink.R, Rgba32.HotPink.G, Rgba32.HotPink.B, 150); |
TPixel color = TestUtils.GetPixelOfNamedColor<TPixel>(newColorName); |
||||
|
image.Mutate(c => c.Fill(color)); |
||||
|
|
||||
|
image.DebugSave(provider, newColorName, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); |
||||
|
image.ComparePixelBufferTo(color); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static readonly TheoryData<bool, string, float, PixelBlenderMode, float> BlendData = |
||||
|
new TheoryData<bool, string, float, PixelBlenderMode, float>() |
||||
|
{ |
||||
|
{ false, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f }, |
||||
|
{ false, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f }, |
||||
|
{ false, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f }, |
||||
|
{ false, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f }, |
||||
|
|
||||
|
{ false, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f }, |
||||
|
{ false, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f }, |
||||
|
{ false, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f }, |
||||
|
{ false, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f }, |
||||
|
|
||||
|
{ false, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f }, |
||||
|
{ false, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f }, |
||||
|
{ false, "Green", 0.5f, PixelBlenderMode.Add, 0.3f }, |
||||
|
{ false, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f }, |
||||
|
|
||||
image.Mutate(x => x |
{ true, "Blue", 0.5f, PixelBlenderMode.Normal, 1.0f }, |
||||
.BackgroundColor(Rgba32.Blue) |
{ true, "Blue", 1.0f, PixelBlenderMode.Normal, 0.5f }, |
||||
.Fill(color)); |
{ true, "Green", 0.5f, PixelBlenderMode.Normal, 0.3f }, |
||||
image.Save($"{path}/Opacity.png"); |
{ true, "HotPink", 0.8f, PixelBlenderMode.Normal, 0.8f }, |
||||
|
|
||||
//shift background color towards forground color by the opacity amount
|
{ true, "Blue", 0.5f, PixelBlenderMode.Multiply, 1.0f }, |
||||
var mergedColor = new Rgba32(Vector4.Lerp(Rgba32.Blue.ToVector4(), Rgba32.HotPink.ToVector4(), 150f / 255f)); |
{ true, "Blue", 1.0f, PixelBlenderMode.Multiply, 0.5f }, |
||||
|
{ true, "Green", 0.5f, PixelBlenderMode.Multiply, 0.3f }, |
||||
|
{ true, "HotPink", 0.8f, PixelBlenderMode.Multiply, 0.8f }, |
||||
|
|
||||
|
{ true, "Blue", 0.5f, PixelBlenderMode.Add, 1.0f }, |
||||
|
{ true, "Blue", 1.0f, PixelBlenderMode.Add, 0.5f }, |
||||
|
{ true, "Green", 0.5f, PixelBlenderMode.Add, 0.3f }, |
||||
|
{ true, "HotPink", 0.8f, PixelBlenderMode.Add, 0.8f }, |
||||
|
}; |
||||
|
|
||||
using (PixelAccessor<Rgba32> sourcePixels = image.Lock()) |
[Theory] |
||||
|
[WithSolidFilledImages(nameof(BlendData), 16, 16, "Red", PixelTypes.Rgba32)] |
||||
|
public void BlendFillColorOverBackround<TPixel>( |
||||
|
TestImageProvider<TPixel> provider, |
||||
|
bool triggerFillRegion, |
||||
|
string newColorName, |
||||
|
float alpha, |
||||
|
PixelBlenderMode blenderMode, |
||||
|
float blendPercentage) |
||||
|
where TPixel : struct, IPixel<TPixel> |
||||
|
{ |
||||
|
var vec = TestUtils.GetPixelOfNamedColor<RgbaVector>(newColorName).ToVector4(); |
||||
|
vec.W = alpha; |
||||
|
|
||||
|
TPixel fillColor = default; |
||||
|
fillColor.PackFromVector4(vec); |
||||
|
|
||||
|
using (Image<TPixel> image = provider.GetImage()) |
||||
|
{ |
||||
|
TPixel bgColor = image[0, 0]; |
||||
|
|
||||
|
var options = new GraphicsOptions(false) |
||||
|
{ |
||||
|
BlenderMode = blenderMode, |
||||
|
BlendPercentage = blendPercentage |
||||
|
}; |
||||
|
|
||||
|
if (triggerFillRegion) |
||||
|
{ |
||||
|
var region = new ShapeRegion(new RectangularPolygon(0, 0, 16, 16)); |
||||
|
|
||||
|
image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor), region)); |
||||
|
} |
||||
|
else |
||||
{ |
{ |
||||
Assert.Equal(mergedColor, sourcePixels[9, 9]); |
image.Mutate(c => c.Fill(options, new SolidBrush<TPixel>(fillColor))); |
||||
Assert.Equal(mergedColor, sourcePixels[199, 149]); |
|
||||
} |
} |
||||
|
|
||||
|
var testOutputDetails = new |
||||
|
{ |
||||
|
triggerFillRegion = triggerFillRegion, |
||||
|
newColorName = newColorName, |
||||
|
alpha = alpha, |
||||
|
blenderMode = blenderMode, |
||||
|
blendPercentage = blendPercentage |
||||
|
}; |
||||
|
|
||||
|
image.DebugSave( |
||||
|
provider, |
||||
|
testOutputDetails, |
||||
|
appendPixelTypeToFileName: false, |
||||
|
appendSourceFileOrDescription: false); |
||||
|
|
||||
|
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(blenderMode); |
||||
|
TPixel expectedPixel = blender.Blend(bgColor, fillColor, blendPercentage); |
||||
|
|
||||
|
image.ComparePixelBufferTo(expectedPixel); |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
Loading…
Reference in new issue