Search code examples
c#imagesharp

Draw text in an ArcLineSegment counter-clockwise with ImageSharp


cross-posting this from the ImageSharp.Drawing GitHub discussion since it's still unanswered after 3 weeks

I'm trying to replicate this:

drawing circle and text in the lower arc

With this code:

        public void Draw(string text)
        {
            var imageSize = 300;
            var fontSize = 18;
            var border = 10;
            var color = Color.ParseHex("#808080");
            FontFamily family = SystemFonts.Get("Arial");
            Font font = new(family, fontSize);
            var radius = imageSize / 2;
            var center = new PointF(radius, radius);

            var img = Image<Rgba32>.Load(@"C:\Users\sergi\Downloads\comet.png");
            img.Mutate(o => o.Resize(new Size(imageSize, imageSize)));
            var topSegment = new ArcLineSegment(center, new SizeF(radius-border, radius-border), 0, -220, 260);
            PathBuilder pathBuilder = new PathBuilder();
            pathBuilder.AddSegment(topSegment);

            var textSegment = new ArcLineSegment(center, new SizeF(radius- fontSize/2 - border, radius- fontSize/2-border), 0, -220, -100);
            IPath textShape = new Polygon(textSegment);
            TextOptions textOptions = new(font)
            {
                WrappingLength = textShape.ComputeLength(),
                VerticalAlignment = VerticalAlignment.Top,
                HorizontalAlignment = HorizontalAlignment.Left,
                TextAlignment = TextAlignment.Center,
                TextDirection = TextDirection.LeftToRight
                
            };
            DrawingOptions options = new()
            {
                GraphicsOptions = new()
                {
                    ColorBlendingMode = PixelColorBlendingMode.Multiply
                }
            };
            IPen pen = Pens.Solid(color, 1);
            img.Mutate(x => x.Draw(color, 2, pathBuilder.Build()));
            img.Mutate(x => x.Draw(color, 2, new PathBuilder().AddSegment(textSegment).Build()));

            IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, textShape, textOptions);
            img.Mutate(i => i.Fill(color, glyphs));
            string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine(
                "test.png")));
            IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
            img.Save(fullPath);
            Console.WriteLine($"Saved to {fullPath}");
        }

I get to this result:

result

How do I make the text use the whole space so that the center alignment will actually put the text in the center of the arc?

I experimented with different segments, I have the same issue with a simpler line segment:

line segment example

var textSegment = new LinearLineSegment(new PointF(50, 150), new PointF(250, 150));
IPath textShape = new Polygon(textSegment);
TextOptions textOptions = new(font)
{
    //WrappingLength = textShape.ComputeLength(),
    VerticalAlignment = VerticalAlignment.Top,
    HorizontalAlignment = HorizontalAlignment.Center,
    TextAlignment = TextAlignment.Center,
    
};

Similarly TextAligment has no effect and having HorizontalAligment.Center will cause the text to be aligned in the middle of the segment start point: test

What is also strange is that the textShape.ComputeLength() returns 400 in the above example, whereas the segment is only 200 long.

Thanks a lot


Solution

  • Having HorizontalAligment.Center will cause the text to be aligned in the middle of the segment start point

    I believe this is expected behavior as HorizontalAligment is designed to be relative to segment start point.

    Similarly TextAligment has no effect

    I believe this is also expected behavior. Text is drawing along with IPath. Making text in the center of a path is a hard problem. Thinking a generic case of finding the middle point of an arbitrary curve (and putting some text there).

    However, your case is specific to a curve that is part of a circle. We can find the smallest curve that encompassed the whole text, and with that lowest sweep angle of that curve, we can do some simple math to center the text.

    public void Draw(string text)
    {
        var imageSize = 300;
        var fontSize = 18;
        var border = 10;
        var color = Color.ParseHex("#808080");
        FontFamily family = SystemFonts.Get("Arial");
        Font font = new(family, fontSize);
        var radius = imageSize / 2;
        var center = new PointF(radius, radius);
    
        var img = Image<Rgba32>.Load(@"C:\tmp\comet.png");
        img.Mutate(o => o.Resize(new Size(imageSize, imageSize)));
        var topSegment = new ArcLineSegment(center, new SizeF(radius - border, radius - border), 0, -220, 260);
        PathBuilder pathBuilder = new PathBuilder();
        pathBuilder.AddSegment(topSegment);
    
        var bottomSegment = new ArcLineSegment(center, new SizeF(radius - fontSize / 2 - border, radius - fontSize / 2 - border), 0, -220, -100);
    
        // Find the smallest curve encompassing the whole text
        var textSweepAngle = FindLowestSweepAngle(text, font, new SizeF(radius - fontSize / 2 - border, radius - fontSize / 2 - border));
    
        var fullSweepAngle = 100;
        var gapSweepAngle = (fullSweepAngle - textSweepAngle) / 2;
        var textStartAngle = -220 - gapSweepAngle;
    
        var textSegment = new ArcLineSegment(center, new SizeF(radius - fontSize / 2 - border, radius - fontSize / 2 - border), 0, textStartAngle, -textSweepAngle);
    
        IPath textShape = new Polygon(textSegment);
        TextOptions textOptions = new(font)
        {
            WrappingLength = textShape.ComputeLength(),
            VerticalAlignment = VerticalAlignment.Top,
            HorizontalAlignment = HorizontalAlignment.Left,
            TextAlignment = TextAlignment.Start,
            TextDirection = TextDirection.LeftToRight
        };
        DrawingOptions options = new()
        {
            GraphicsOptions = new()
            {
                ColorBlendingMode = PixelColorBlendingMode.Multiply
            }
        };
        IPen pen = Pens.Solid(color, 1);
        img.Mutate(x => x.Draw(color, 2, pathBuilder.Build()));
        img.Mutate(x => x.Draw(color, 2, new PathBuilder().AddSegment(bottomSegment).Build()));
    
        IPathCollection glyphs = TextBuilder.GenerateGlyphs(text, textShape, textOptions);
        img.Mutate(i => i.Fill(color, glyphs));
        string fullPath = IOPath.GetFullPath(IOPath.Combine("Output", IOPath.Combine("test.png")));
        IODirectory.CreateDirectory(IOPath.GetDirectoryName(fullPath));
        img.Save(fullPath);
        Console.WriteLine($"Saved to {fullPath}");
    }
    
    private float FindLowestSweepAngle(string text, Font font, SizeF radius)
    {
        var low = 0.1f;
        var high = 359.9f;
        var step = 0.1f;
    
        while (low < high)
        {
            var mid = (low + high) / 2;
            var arcLineSegment = new ArcLineSegment(PointF.Empty, radius, 0, 0, mid);
            var polygon = new Polygon(arcLineSegment);
    
            TextOptions textOptions = new(font)
            {
                WrappingLength = polygon.ComputeLength(),
                VerticalAlignment = VerticalAlignment.Top,
                HorizontalAlignment = HorizontalAlignment.Left,
                TextAlignment = TextAlignment.Start,
                TextDirection = TextDirection.LeftToRight
            };
    
            try
            {
                TextBuilder.GenerateGlyphs(text, polygon, textOptions);
                high = mid - step; // Curve is too big. Keep finding a smaller one
            }
            catch // InvalidOperationException: Should always reach a point along the path
            {
                low = mid + step; // Curve is too small to hold the whole text
            }
        }
    
        return low;
    }
    

    The result is shown below.

    enter image description here