SixLabors / ImageSharp.Drawing

:pen: Extensions to ImageSharp containing a cross-platform 2D polygon manipulation API and drawing operations.
https://sixlabors.com/products/imagesharp-drawing/
Other
285 stars 39 forks source link

IndexOutOfRangeException when drawing line near top when Stroke > 1.5f #108

Closed atruskie closed 3 years ago

atruskie commented 3 years ago

Prerequisites

Description

Drawing a horizontal line across the top edge of an image results in an exception if the stroke width is greater than 1.5f and antialiasing is disabled.

Steps to Reproduce

The following test code was run and adapted from

https://github.com/SixLabors/ImageSharp.Drawing/blob/48a803e51df044f33e061fc140e81aec5b3aa310/tests/ImageSharp.Drawing.Tests/Issues/Issue_28.cs#L18-L35

        [Fact]
        public void DrawingLineAtTopWith1point5pxStrokeShouldDisplay()
        {
            using var image = new Image<Rgba32>(Configuration.Default, 100, 100, Color.Black);
            image.Mutate(x => x
                    .SetGraphicsOptions(g => g.Antialias = false)
                    .DrawLines(
                        this.red,
                        1.5f,
                        new PointF(0, 0),
                        new PointF(100, 0)
                    ));

            var locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0));
            Assert.All(locations, l =>
            {
                Assert.Equal(this.red, image[l.x, l.y]);
            });
        }

        [Fact]
        public void DrawingLineAtTopWith2pxStrokeShouldDisplay()
        {
            using var image = new Image<Rgba32>(Configuration.Default, 100, 100, Color.Black);
            image.Mutate(x => x
                    .SetGraphicsOptions(g => g.Antialias = false)
                    .DrawLines(
                        this.red,
                        2f,
                        new PointF(0, 0),
                        new PointF(100, 0)
                    ));

            var locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0));
            Assert.All(locations, l =>
            {
                Assert.Equal(this.red, image[l.x, l.y]);
            });
        }

        [Fact]
        public void DrawingLineAtTopWith3pxStrokeShouldDisplay()
        {
            using var image = new Image<Rgba32>(Configuration.Default, 100, 100, Color.Black);
            image.Mutate(x => x
                    .SetGraphicsOptions(g => g.Antialias = false)
                    .DrawLines(
                        this.red,
                        3f,
                        new PointF(0, 0),
                        new PointF(100, 0)
                    ));

            var locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0))
                .Concat(Enumerable.Range(0, 100).Select(i => (x: i, y: 1)));
            Assert.All(locations, l =>
            {
                Assert.Equal(this.red, image[l.x, l.y]);
            });
        }

Then: C:\Work\Github\ImageSharp.Drawing\tests\ImageSharp.Drawing.Tests> dotnet test -c Release --filter "Issue_28"

The existing tests pass, the altered stroke width tests fail:

<snip>
Error Message:
   SixLabors.ImageSharp.ImageProcessingException : An error occurred when processing the image using FillRegionProcessor`1. See the inner exception for more detail.
---- SixLabors.ImageSharp.ImageProcessingException : An error occurred when processing the image using FillRegionProcessor`1. See the inner exception for more detail.
-------- System.IndexOutOfRangeException : Index was outside the bounds of the array.
  Stack Trace:
     at SixLabors.ImageSharp.Processing.Processors.ImageProcessor`1.SixLabors.ImageSharp.Processing.Processors.IImageProcessor<TPixel>.Execute()
   at SixLabors.ImageSharp.Processing.DefaultImageProcessorContext`1.ApplyProcessor(IImageProcessor processor, Rectangle rectangle)
   at SixLabors.ImageSharp.Processing.DefaultImageProcessorContext`1.ApplyProcessor(IImageProcessor processor)
   at SixLabors.ImageSharp.Drawing.Processing.DrawLineExtensions.DrawLines(IImageProcessingContext source, IBrush brush, Single thickness, PointF[] points) in C:\Work\Github\ImageSharp.Drawing\src\ImageSharp.Drawing\Processing\Extensions\DrawLineExtensions.cs:line 43
   at SixLabors.ImageSharp.Drawing.Tests.Issues.Issue_28.<>c__DisplayClass3_0.<DrawingLineAtTopWith2pxStrokeShouldDisplay>b__0(IImageProcessingContext x) in C:\Work\Github\ImageSharp.Drawing\tests\ImageSharp.Drawing.Tests\Issues\Issue_28.cs:line 61
   at SixLabors.ImageSharp.Processing.ProcessingExtensions.Mutate[TPixel](Image`1 source, Configuration configuration, Action`1 operation)
   at SixLabors.ImageSharp.Drawing.Tests.Issues.Issue_28.DrawingLineAtTopWith2pxStrokeShouldDisplay() in C:\Work\Github\ImageSharp.Drawing\tests\ImageSharp.Drawing.Tests\Issues\Issue_28.cs:line 61
----- Inner Stack Trace -----
   at SixLabors.ImageSharp.Processing.Processors.ImageProcessor`1.Apply(ImageFrame`1 source)
   at SixLabors.ImageSharp.Processing.Processors.ImageProcessor`1.SixLabors.ImageSharp.Processing.Processors.IImageProcessor<TPixel>.Execute()
----- Inner Stack Trace -----
   at SixLabors.ImageSharp.Drawing.Shapes.Rasterization.PolygonScanner.SkipEdgesBeforeMinY() in C:\Work\Github\ImageSharp.Drawing\src\ImageSharp.Drawing\Shapes\Rasterization\PolygonScanner.cs:line 149
   at SixLabors.ImageSharp.Drawing.Shapes.Rasterization.PolygonScanner.Init() in C:\Work\Github\ImageSharp.Drawing\src\ImageSharp.Drawing\Shapes\Rasterization\PolygonScanner.cs:line 127
   at SixLabors.ImageSharp.Drawing.Shapes.Rasterization.PolygonScanner.Create(IPath polygon, Int32 minY, Int32 maxY, Int32 subsampling, IntersectionRule intersectionRule, MemoryAllocator allocator) in C:\Work\Github\ImageSharp.Drawing\src\ImageSharp.Drawing\Shapes\Rasterization\PolygonScanner.cs:line 104
   at SixLabors.ImageSharp.Drawing.Processing.Processors.Drawing.FillRegionProcessor`1.OnFrameApply(ImageFrame`1 source) in C:\Work\Github\ImageSharp.Drawing\src\ImageSharp.Drawing\Processing\Processors\Drawing\FillRegionProcessor{TPixel}.cs:line 74
   at SixLabors.ImageSharp.Processing.Processors.ImageProcessor`1.Apply(ImageFrame`1 source)
<snip>
Failed!  - Failed:     3, Passed:     4, Skipped:     0, Total:     7, Duration: 154 ms

System Configuration

Seems to be related to the new PolygonScanner SkipEdgesBeforeMinY code.

antonfirsov commented 3 years ago

We are likely dealing with some fallout from #96. (Not unexpected considering the volume of the change.) Need to look into this.

atruskie commented 3 years ago

I just realized the Antialiasing = false part of this issue is misleading. The bug occurs either with ( Antialiasing = true) or without (Antialiasing = false) antialiasing.

JimBobSquarePants commented 3 years ago

@antonfirsov I can get all the tests to pass including the new ones listed here by adding && i0 < this.edges.Length to the while loop, however I have no idea whether this is a valid fix. (I'm really not confident.)

private void SkipEdgesBeforeMinY()
{
    if (this.edges.Length == 0)
    {
        return;
    }

    this.SubPixelY = this.edges[this.sorted0[0]].Y0;

    int i0 = 1;
    int i1 = 0;

    // Do fake scans for the lines belonging to edge start and endpoints before minY
    while (this.SubPixelY < this.minY && i0 < this.edges.Length)
    {
        this.EnterEdges();
        this.LeaveEdges();
        this.activeEdges.RemoveLeavingEdges();

        float y0 = this.edges[this.sorted0[i0]].Y0;
        float y1 = this.edges[this.sorted1[i1]].Y1;

        if (y0 < y1)
        {
            this.SubPixelY = y0;
            i0++;
        }
        else
        {
            this.SubPixelY = y1;
            i1++;
        }
    }
}
Robertofon commented 3 years ago

Just to mention. Also happens to me. I have AntiAlias= true and this happens if I draw an ellipsis with one part outside of the image (eg. R=3 and X,Y =0,3) or drawing lines (0,0)-(0,30) with line thickness = 2 for example.

JimBobSquarePants commented 3 years ago

@Robertofon See https://github.com/SixLabors/ImageSharp.Drawing/issues/108#issuecomment-736126188

OleksandrKrutykh commented 3 years ago

Just for your information, the issue can be reproduced even if the thickness of the line is 1. I got an ImageProcessingException when running this piece of code on Windows 10 v1903:

        static void Main()
        {
            Image<Rgba32> image = new Image<Rgba32>(width: 500, height: 400);
            var startPoint = new PointF(493.55447f, -87.83895f);
            var endPoint = new PointF(500.0656f, 174.81201f);
            image.Mutate(x => x.DrawLines(Color.Black, thickness: 1, startPoint, endPoint));
        }

The minimal Visual Studio solution to reproduce the issue: DrawLinesIssue.zip

antonfirsov commented 3 years ago

This method was developed in rush, after realizing that I need to handle the cases where lines are outside the drawing bounds ... looks like testing was insufficient.

I need more time to debug into that crazy code and understand exactly why is the overflow happening. We should not block on this, so @JimBobSquarePants feel free to raise a PR with your fix, since it seems to make things better. Just let's include tests from both the OP and https://github.com/SixLabors/ImageSharp.Drawing/issues/108#issuecomment-755988422, and also have a look at the outputs.

JimBobSquarePants commented 3 years ago

@antonfirsov Looks like my fix is bad. It's offsetting the values not actually processing the virtual edges.

What I'm seeing during debugging given a stroke width of 3.

while (this.SubPixelY < this.minY)
{
    this.EnterEdges();
    this.LeaveEdges();
    this.activeEdges.RemoveLeavingEdges();

    float y0 = this.edges[this.sorted0[i0]].Y0; // Always -1
    float y1 = this.edges[this.sorted1[i1]].Y1; // Always 2

    // Always true
    if (y0 < y1)
    {
        this.SubPixelY = y0;
        i0++;
    }
    else
    {
        this.SubPixelY = y1;
        i1++;
    }
}
JimBobSquarePants commented 3 years ago

Looks like I have a fix. Will do some final testing