7

I am drawing strings to images. The size of the images is dynamic, or in other words, the images are as large as necessary to display the strings. To achieve that I am measuring the size with Graphics.MeasureString() before rendering the text with Graphics.DrawString().

That's all fine till the point where rotation comes into play. Until now I am drawing the string to the bitmap and rotate the whole bitmap I get as an result.

The problem is that I have a very limited color palette and no mixed colors. So I have to avoid any kind of antialiasing, which is only possible by using InterpolationMode.NearestNeighbor by rotating the text bitmap. While that makes sure no unwanted color is rendered the result is indeed very ugly (from the users point of view).

My thought: it should be possible to draw the text to the bitmap by rotating it with Graphics.RotateTransform() and avoiding clipping, isn't it?

Because I have to define the size of the image to draw on first, and because this size increases by rotating, I have no clue how to get this done.

Any help is very appreciated!

asp_net
  • 3,567
  • 3
  • 31
  • 58
  • You want to rotate to any arbitary angle (e.g. 37.5 degrees, 118 degrees etc.), or only to right angles (90, 180, 270)? – MusiGenesis Apr 04 '11 at 20:46
  • It is a mystery to me why you expect the text to look better by rotating it. It won't, you haven't solved the lack of anti-aliasing support. So you're stuck with the blobby pixels. – Hans Passant Apr 04 '11 at 21:10
  • @Hans Passant: it is a bit, not as much as I wish it would be, but it is. By using RotateTransform it's not as tattered as with rotating the bitmap with InterpolationMode.NearestNeighbor. – asp_net Apr 04 '11 at 21:16
  • Gotcha, yes that's a problem. – Hans Passant Apr 04 '11 at 21:19

2 Answers2

4

This code will give you an idea:

    public void DrawText(bool debug, Graphics g, string text, Font font, Brush brush, StringFormat format, float x, float y, float width, float height, float rotation)
    {
        float centerX = width / 2;
        float centerY = height / 2;

        if (debug)
        {
            g.FillEllipse(Brushes.Green, centerX - 5f, centerY - 5f, 10f, 10f);
        }

        GraphicsState gs = g.Save();

        Matrix mat = new Matrix();
        mat.RotateAt(rotation, new PointF(centerX, centerY), MatrixOrder.Append);

        g.Transform = mat;

        SizeF szf = g.MeasureString(text, font);

        g.DrawString(text, font, brush, (width / 2) - (szf.Width / 2), (height / 2) - (szf.Height / 2), format);

        g.Restore(gs);
    }

Here is a method to measure rotated text bounds by using GraphicsPath. Logic is simple, GraphicsPath converts text to point list and then it calculates bounds rectangle.

    public RectangleF GetRotatedTextBounds(string text, Font font, StringFormat format, float rotation, float dpiY)
    {
        GraphicsPath gp = new GraphicsPath();

        float emSize = dpiY * font.Size / 72;

        gp.AddString(text, font.FontFamily, (int)font.Style, emSize, new PointF(0, 0), format);

        Matrix mat = new Matrix();
        mat.Rotate(rotation, MatrixOrder.Append);

        gp.Transform(mat);

        return gp.GetBounds();
    }

Test code:

        float fontSize = 25f;
        float rotation = 30f;

        RectangleF txBounds = GetRotatedTextBounds("TEST TEXT", new Font("Verdana", fontSize, System.Drawing.FontStyle.Bold), StringFormat.GenericDefault, rotation, 96f);

        float inflateValue = 10 * (fontSize / 100f);

        txBounds.Inflate(inflateValue, inflateValue);

        Bitmap bmp = new System.Drawing.Bitmap((int)txBounds.Width, (int)txBounds.Height);
        using (Graphics gr = Graphics.FromImage(bmp))
        {
            gr.Clear(Color.White);
            DrawText(true, gr, "TEST TEXT", new Font("Verdana", fontSize, System.Drawing.FontStyle.Bold), Brushes.Red, new StringFormat(System.Drawing.StringFormatFlags.DisplayFormatControl), 0, 0, txBounds.Width, txBounds.Height, rotation);
        }
HABJAN
  • 9,212
  • 3
  • 35
  • 59
  • Hi Habjan, thanks for your effort, but I don't know the final image size before painting (the image is always as large as the text is). Parameters like font, font size and so on are dynamic. – asp_net Apr 04 '11 at 22:50
  • @asp_net: I changed both methods (DrawText, GetRotatedTextBounds). Take a look now. – HABJAN Apr 05 '11 at 10:24
  • Thanks Habjan! I already figured it out this morning, see my answer below. – asp_net Apr 05 '11 at 10:42
2

My solution as an executable MVC action:

public class ImageController : Controller
{

    public ActionResult Test()
    {

        var text = DateTime.Now.ToString();
        var font = new Font("Arial", 20, FontStyle.Regular);
        var angle = 233;

        SizeF textSize = GetEvenTextImageSize(text, font);

        SizeF imageSize;

        if (angle == 0)
            imageSize = textSize;
        else
            imageSize = GetRotatedTextImageSize(textSize, angle);

        using (var canvas = new Bitmap((int)imageSize.Width, (int)imageSize.Height))
        {

            using(var graphics = Graphics.FromImage(canvas))
            {

                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                graphics.SmoothingMode = SmoothingMode.HighQuality;
                graphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;

                SizeF textContainerSize = graphics.VisibleClipBounds.Size;
                graphics.TranslateTransform(textContainerSize.Width / 2, textContainerSize.Height / 2);
                graphics.RotateTransform(angle);

                graphics.DrawString(text, font, Brushes.Black, -(textSize.Width / 2), -(textSize.Height / 2));

            }

            var stream = new MemoryStream();
            canvas.Save(stream, ImageFormat.Png);
            stream.Seek(0, SeekOrigin.Begin);
            return new FileStreamResult(stream, "image/png");

        }

    }

    private static SizeF GetEvenTextImageSize(string text, Font font)
    {
        using (var image = new Bitmap(1, 1, PixelFormat.Format32bppArgb))
        {
            using (Graphics graphics = Graphics.FromImage(image))
            {
                return graphics.MeasureString(text, font);
            }
        }
    }

    private static SizeF GetRotatedTextImageSize(SizeF fontSize, int angle)
    {

        // Source: http://www.codeproject.com/KB/graphics/rotateimage.aspx

        double theta = angle * Math.PI / 180.0;

        while (theta < 0.0)
            theta += 2 * Math.PI;

        double adjacentTop, oppositeTop;
        double adjacentBottom, oppositeBottom;

        if ((theta >= 0.0 && theta < Math.PI / 2.0) || (theta >= Math.PI && theta < (Math.PI + (Math.PI / 2.0))))
        {
            adjacentTop = Math.Abs(Math.Cos(theta)) * fontSize.Width;
            oppositeTop = Math.Abs(Math.Sin(theta)) * fontSize.Width;
            adjacentBottom = Math.Abs(Math.Cos(theta)) * fontSize.Height;
            oppositeBottom = Math.Abs(Math.Sin(theta)) * fontSize.Height;
        }
        else
        {
            adjacentTop = Math.Abs(Math.Sin(theta)) * fontSize.Height;
            oppositeTop = Math.Abs(Math.Cos(theta)) * fontSize.Height;
            adjacentBottom = Math.Abs(Math.Sin(theta)) * fontSize.Width;
            oppositeBottom = Math.Abs(Math.Cos(theta)) * fontSize.Width;
        }

        int nWidth = (int)Math.Ceiling(adjacentTop + oppositeBottom);
        int nHeight = (int)Math.Ceiling(adjacentBottom + oppositeTop);

        return new SizeF(nWidth, nHeight);

    }

}
asp_net
  • 3,567
  • 3
  • 31
  • 58