From e7f368f83654726b022ed8bf63afb23c499ec623 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 9 Dec 2023 13:31:01 +0000 Subject: [PATCH 1/3] correctly apply and pass thru the correct negative offset --- .../Processing/ImageBrush.cs | 61 ++++++++++++--- .../Drawing/ClipPathProcessor{TPixel}.cs | 16 +++- .../Processors/Drawing/FillPathProcessor.cs | 4 +- .../Drawing/FillPathProcessor{TPixel}.cs | 2 +- .../Drawing/FillProcessor{TPixel}.cs | 2 +- .../Drawing/ClipTests.cs | 1 + .../Drawing/FillImageBrushTests.cs | 74 ++++++++++++++++++- .../ClipTests/Clip_offset_x-20_y-100.png | 3 + .../CanDrawOffsetImage_Rgba32.png | 3 + .../CanOffsetImage_Rgba32.png | 3 + .../CanOffsetViaBrushImage_Rgba32.png | 3 + 11 files changed, 156 insertions(+), 16 deletions(-) create mode 100644 tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png create mode 100644 tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png create mode 100644 tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 5d8075e3..0bcf2082 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -21,6 +21,11 @@ public class ImageBrush : Brush /// private readonly RectangleF region; + /// + /// The an offet to apply to the source image while applying the imagebrush + /// + private readonly Point offet; + /// /// Initializes a new instance of the class. /// @@ -33,12 +38,44 @@ public ImageBrush(Image image) /// /// Initializes a new instance of the class. /// - /// The source image. - /// The region of interest within the source image to draw. - public ImageBrush(Image image, RectangleF region) + /// The image. + /// + /// An offset to apply the to image image while drawing apply the texture. + /// + public ImageBrush(Image image, Point offset) + : this(image, image.Bounds, offset) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + internal ImageBrush(Image image, RectangleF region) + : this(image, region, Point.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + /// + /// An offset to apply the to image image while drawing apply the texture. + /// + internal ImageBrush(Image image, RectangleF region, Point offet) { this.image = image; this.region = RectangleF.Intersect(image.Bounds, region); + this.offet = offet; } /// @@ -64,11 +101,11 @@ public override BrushApplicator CreateApplicator( { if (this.image is Image specificImage) { - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, false); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offet, false); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, true); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offet, true); } /// @@ -107,6 +144,7 @@ private class ImageBrushApplicator : BrushApplicator /// The image. /// The region of the target image we will be drawing to. /// The region of the source image we will be using to source pixels to draw from. + /// An offset to apply to the texture while drawing. /// Whether to dispose the image on disposal of the applicator. public ImageBrushApplicator( Configuration configuration, @@ -115,6 +153,7 @@ public ImageBrushApplicator( Image image, RectangleF targetRegion, RectangleF sourceRegion, + Point offset, bool shouldDisposeImage) : base(configuration, options, target) { @@ -124,8 +163,8 @@ public ImageBrushApplicator( this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion); - this.offsetY = (int)MathF.Max(MathF.Floor(targetRegion.Top), 0); - this.offsetX = (int)MathF.Max(MathF.Floor(targetRegion.Left), 0); + this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y; + this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X; } internal TPixel this[int x, int y] @@ -166,14 +205,18 @@ public override void Apply(Span scanline, int x, int y) Span overlaySpan = overlay.Memory.Span; int offsetX = x - this.offsetX; - int sourceY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y; + int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height + + this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height + + this.sourceRegion.Y; Span sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY); for (int i = 0; i < scanline.Length; i++) { amountSpan[i] = scanline[i] * this.Options.BlendPercentage; - int sourceX = ((i + offsetX) % this.sourceRegion.Width) + this.sourceRegion.X; + int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width + + this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width + + this.sourceRegion.X; overlaySpan[i] = sourceRow[sourceX]; } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs index a9ca8ebb..c3ef3961 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/ClipPathProcessor{TPixel}.cs @@ -39,7 +39,21 @@ public void Execute() // Use an image brush to apply cloned image as the source for filling the shape. // We pass explicit bounds to avoid the need to crop the clone; RectangleF bounds = this.definition.Region.Bounds; - var brush = new ImageBrush(clone, bounds); + + // add some clamping offsets to the brush to account for the target drawing location due to the cloned image not fill the image as expected + var offsetX = 0; + var offsetY = 0; + if (bounds.X < 0) + { + offsetX = -(int)MathF.Floor(bounds.X); + } + + if (bounds.Y < 0) + { + offsetY = -(int)MathF.Floor(bounds.Y); + } + + var brush = new ImageBrush(clone, bounds, new Point(offsetX, offsetY)); // Grab hold of an image processor that can fill paths with a brush to allow it to do the hard pixel pushing for us var processor = new FillPathProcessor(this.definition.Options, brush, this.definition.Region); diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs index 01449d16..63c11d64 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor.cs @@ -56,11 +56,9 @@ public IImageProcessor CreatePixelSpecificProcessor(Configuratio var rect = (Rectangle)rectF; if (!this.Options.GraphicsOptions.Antialias || rectF == rect) { - var interest = Rectangle.Intersect(sourceRectangle, rect); - // Cast as in and back are the same or we are using anti-aliasing return new FillProcessor(this.Options, this.Brush) - .CreatePixelSpecificProcessor(configuration, source, interest); + .CreatePixelSpecificProcessor(configuration, source, rect); } } diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs index 8d1aac68..f9426b96 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillPathProcessor{TPixel}.cs @@ -74,7 +74,7 @@ protected override void OnFrameApply(ImageFrame source) subpixelCount = Math.Max(subpixelCount, graphicsOptions.AntialiasSubpixelDepth); } - using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, interest); + using BrushApplicator applicator = brush.CreateApplicator(configuration, graphicsOptions, source, this.bounds); int scanlineWidth = interest.Width; MemoryAllocator allocator = this.Configuration.MemoryAllocator; bool scanlineDirty = true; diff --git a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs index 67fff8fb..c135152f 100644 --- a/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Processors/Drawing/FillProcessor{TPixel}.cs @@ -58,7 +58,7 @@ protected override void OnFrameApply(ImageFrame source) configuration, options, source, - interest); + this.SourceRectangle); amount.Memory.Span.Fill(1F); diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs index 62f0109d..8a38699c 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/ClipTests.cs @@ -14,6 +14,7 @@ public class ClipTests [Theory] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 0, 0, 0.5)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -20, 0.5)] + [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, -20, -100, 0.5)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 20, 20, 0.5)] [WithBasicTestPatternImages(250, 350, PixelTypes.Rgba32, 40, 60, 0.2)] public void Clip(TestImageProvider provider, float dx, float dy, float sizeMult) diff --git a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs b/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs index ec2e3e6a..eb8211a0 100644 --- a/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Drawing/FillImageBrushTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Drawing; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -72,11 +74,81 @@ public void CanDrawPortraitImage(TestImageProvider provider) overlay.Mutate(c => c.Crop(new Rectangle(0, 0, 90, 125))); - ImageBrush brush = new(overlay); + var brush = new ImageBrush(overlay); + background.Mutate(c => c.Fill(brush)); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] + public void CanOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + var brush = new ImageBrush(overlay); + background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); + background.Mutate(c => c.Fill(brush, new RectangularPolygon(-100, 200, 500, 200))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithTestPatternImage(400, 400, PixelTypes.Rgba32)] + public void CanOffsetViaBrushImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + using Image overlay = Image.Load(data); + + var brush = new ImageBrush(overlay); + var brushOffset = new ImageBrush(overlay, new Point(100, 0)); + background.Mutate(c => c.Fill(brush, new RectangularPolygon(0, 0, 400, 200))); + background.Mutate(c => c.Fill(brushOffset, new RectangularPolygon(0, 200, 400, 200))); + + background.DebugSave(provider, appendSourceFileOrDescription: false); + background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(1000, 1000, "White", PixelTypes.Rgba32)] + public void CanDrawOffsetImage(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + byte[] data = TestFile.Create(TestImages.Png.Ducky).Bytes; + using Image background = provider.GetImage(); + + using Image templateImage = Image.Load(data); + using Image finalTexture = BuildMultiRowTexture(templateImage); + + finalTexture.Mutate(c => c.Resize(100, 200)); + + ImageBrush brush = new(finalTexture); background.Mutate(c => c.Fill(brush)); background.DebugSave(provider, appendSourceFileOrDescription: false); background.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); + + Image BuildMultiRowTexture(Image sourceTexture) + { + int halfWidth = sourceTexture.Width / 2; + + Image final = sourceTexture.Clone(x => x.Resize(new ResizeOptions + { + Size = new Size(templateImage.Width, templateImage.Height * 2), + Position = AnchorPositionMode.TopLeft, + Mode = ResizeMode.Pad, + }) + .DrawImage(templateImage, new Point(halfWidth, sourceTexture.Height), new Rectangle(0, 0, halfWidth, sourceTexture.Height), 1) + .DrawImage(templateImage, new Point(0, templateImage.Height), new Rectangle(halfWidth, 0, halfWidth, sourceTexture.Height), 1)); + return final; + } } [Theory] diff --git a/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png new file mode 100644 index 00000000..6195a230 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ClipTests/Clip_offset_x-20_y-100.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ec585523a17e1780408ca643f38261159cd331d948fefd120241482731cb0ed +size 5702 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png new file mode 100644 index 00000000..71a0bc8f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanDrawOffsetImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20c4f6324712fcc2e6b6cf012c169290a01a9199eb96ba3691550b75b2b2b524 +size 150296 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png new file mode 100644 index 00000000..5acd7f8f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b3e455c552537815ca1d5c0699b5fa36bd1963a0a28a06c8cfcf5fb8c5c884f +size 251984 diff --git a/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png new file mode 100644 index 00000000..be90717e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/FillImageBrushTests/CanOffsetViaBrushImage_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fd4fd80d6bfb522b21d884fc6aba6bb5049d5feeb5a05c8b86e087a7229a440 +size 299061 From 9269ea985b2cc22fda1f775c8e976120f9ea8d34 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Sat, 9 Dec 2023 13:45:04 +0000 Subject: [PATCH 2/3] typeos --- src/ImageSharp.Drawing/Processing/ImageBrush.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 0bcf2082..838c7461 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -22,9 +22,9 @@ public class ImageBrush : Brush private readonly RectangleF region; /// - /// The an offet to apply to the source image while applying the imagebrush + /// The offet to apply to the source image while applying the imagebrush /// - private readonly Point offet; + private readonly Point offset; /// /// Initializes a new instance of the class. @@ -68,14 +68,14 @@ internal ImageBrush(Image image, RectangleF region) /// The region of interest. /// This overrides any region used to initialize the brush applicator. /// - /// + /// /// An offset to apply the to image image while drawing apply the texture. /// - internal ImageBrush(Image image, RectangleF region, Point offet) + internal ImageBrush(Image image, RectangleF region, Point offset) { this.image = image; this.region = RectangleF.Intersect(image.Bounds, region); - this.offet = offet; + this.offset = offset; } /// @@ -101,11 +101,11 @@ public override BrushApplicator CreateApplicator( { if (this.image is Image specificImage) { - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offet, false); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, false); } specificImage = this.image.CloneAs(); - return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offet, true); + return new ImageBrushApplicator(configuration, options, source, specificImage, region, this.region, this.offset, true); } /// From 4d30fd90fbf303c87a173722066d7b1b81f60981 Mon Sep 17 00:00:00 2001 From: Scott Williams Date: Mon, 11 Dec 2023 11:09:45 +0000 Subject: [PATCH 3/3] make constructors public again --- src/ImageSharp.Drawing/Processing/ImageBrush.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index 838c7461..5a8062e3 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -55,7 +55,7 @@ public ImageBrush(Image image, Point offset) /// The region of interest. /// This overrides any region used to initialize the brush applicator. /// - internal ImageBrush(Image image, RectangleF region) + public ImageBrush(Image image, RectangleF region) : this(image, region, Point.Empty) { } @@ -71,7 +71,7 @@ internal ImageBrush(Image image, RectangleF region) /// /// An offset to apply the to image image while drawing apply the texture. /// - internal ImageBrush(Image image, RectangleF region, Point offset) + public ImageBrush(Image image, RectangleF region, Point offset) { this.image = image; this.region = RectangleF.Intersect(image.Bounds, region);