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
282 stars 38 forks source link

Rotated text gets offset radially #192

Closed replaysMike closed 2 years ago

replaysMike commented 2 years ago

Prerequisites

Description

When rotating text the translation offset doesn't stay put. The text gets shifted in a radial direction depending on the angle in which it is rotated by.

Steps to Reproduce

using (var image = new Image<Rgba32>(300, 200))
{
    string text = "QuickTYZ";
    int rotationAngle = 90;
    Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", 40, Fonts.FontStyle.Regular);

    AffineTransformBuilder builder = new AffineTransformBuilder()
            .AppendRotationDegrees(rotationAngle)
            .AppendTranslation(new PointF(0, 0));
    var drawingOptions = new DrawingOptions
    {
        Transform = builder.BuildMatrix(image.Bounds())
    };

    image.Mutate(c => c.DrawText(drawingOptions, text, font, Brushes.Solid(Color.Red), new PointF(0, 0)));
     image.SaveAsPng(@"./textrotation.png");
}

System Configuration

replaysMike commented 2 years ago

I'm going to attempt to fix this and issue a PR

replaysMike commented 2 years ago

what I've discovered so far is interesting but I'm not really sure if the current implementation is correct or not. I noticed that when calling builder.BuildMatrix() there is a Translation applied equal to the height of the image, and I don't know why that translation exists. It could be a bug in the BuildMatrix() or I could simply be using this wrong.

I can compensate for the seemingly incorrect matrix transform by replacing the Translation transform equal to the font size. This looks right for the 90 degree rotation, however this moves it to the right if the rotation angle = 0 so it's not a great solution:

using (var image = new Image<Rgba32>(300, 200))
{
    string text = "QuickTYZ";
    int rotationAngle = 90;
    int fontSize = 40;
    Fonts.Font font = Fonts.SystemFonts.CreateFont("Arial", fontSize, Fonts.FontStyle.Regular);

    AffineTransformBuilder builder = new AffineTransformBuilder()
            .AppendRotationDegrees(rotationAngle);
    var transform = builder.BuildMatrix(image.Bounds());
    transform.Translation = new System.Numerics.Vector2(fontSize, 0);
    var drawingOptions = new DrawingOptions
    {
        Transform = transform
    };

    image.Mutate(c => c.DrawText(drawingOptions, text, font, Brushes.Solid(Color.Red), new PointF(0, 0)));
    image.SaveAsPng(@"./textrotation.png");
}
JimBobSquarePants commented 2 years ago

Your transform matrix looks incorrect.

AffineTransformBuilder builder = new AffineTransformBuilder()
            .AppendRotationDegrees(rotationAngle)
            .AppendTranslation(new PointF(0, 0));
var transform = builder.BuildMatrix(image.Bounds());

The bounds you are passing should be the bounds of what ever source object you are attempting to transform. We require those bounds in order to perform rotation around the center of the object (so we can perform the correct translations). In your case the bounds should represent the measured bounds of the text you are rendering. It also looks like you're appending an unnecessary transform to the builder.

So given this knowledge your transform should be the following.

TextOptions textOptions = new(font);
FontRectangle bounds = TextMeasurer.Measure(text, textOptions);
AffineTransformBuilder builder = new AffineTransformBuilder().AppendRotationDegrees(rotationAngle);

// We should probably add an extension to FontRectangle to make the conversion to Rectangle easier.
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(bounds.X, bounds.Y, bounds.Width, bounds.Height)));
replaysMike commented 2 years ago

thanks for that @JimBobSquarePants . I confirmed this works. The second transform (translation) was still required, as specifying co-ordinates in the DrawText operation yield unusual results depending on the angle - for example 90 degrees causes those coordinates to be inverted and X/Y is also reversed. But that likely makes sense if they are being applied before the transforms are.

I think an example regarding rotation in the docs would go a long way, as it's not simple to figure out. Having to know the bounds of the text seems like a detail that should be internal to the framework and isn't very obvious. I don't know the design decisions involved but as a user it wasn't simple to figure out. What I would expect to see is something like the following, though I know this yields rotation on the entire image:

image.Mutate(c => c.DrawText(text, font, Brushes.Solid(Color.Red), new PointF(50, 100)).RotateDegrees(90));

Full example of rotating text for those landing on this page:

TextOptions textOptions = new(font);
FontRectangle bounds = TextMeasurer.Measure(text, textOptions);
AffineTransformBuilder builder = new AffineTransformBuilder()
    .AppendRotationDegrees(rotationAngle)
    .AppendTranslation(new PointF(100, 50));;

// We should probably add an extension to FontRectangle to make the conversion to Rectangle easier.
Matrix3x2 transform = builder.BuildMatrix(Rectangle.Round(new RectangleF(bounds.X, bounds.Y, bounds.Width, bounds.Height)));
var drawingOptions = new DrawingOptions
{
    Transform = transform
};
image.Mutate(c => c.DrawText(drawingOptions, text, font, Brushes.Solid(Color.Red), new PointF(0, 0)));
JimBobSquarePants commented 2 years ago

The second transform (translation) was still required, as specifying co-ordinates in the DrawText operation yield unusual results depending on the angle - for example 90 degrees causes those coordinates to be inverted and X/Y is also reversed. But that likely makes sense if they are being applied before the transforms are.

I don't quite follow what you mean here.

.AppendTranslation(new PointF(0, 0))

Means translate by 0,0 not to 0,0 so isn't actually doing anything. The position of the rendered text is determined by the location passed via TextOptions where the vector represents the top/left position of the text bounds.

I think an example regarding rotation in the docs would go a long way, as it's not simple to figure out. Having to know the bounds of the text seems like a detail that should be internal to the framework and isn't very obvious.

We're still very much figuring out the best API so want to hold out on examples however calculating the correct Matrix3x2 transform for things like rotation around a center simply can't be determined without those bounds. Since we accept the raw matrix as a parameter there's no way to internalize the calculation.