Codo
Codo

Reputation: 78825

How to properly left-align text with DrawString?

I'm drawing text to an off-screen bitmap. Unfortunately, the text is not properly left aligned (see image below). The text should touch the left margin (blue line) but is off by a few pixel. The distance grows with the text size.

Left alignment

How can I get rid of this distance?

I'm using .NET Framework 4.6.1. But it seems to more a general GDI+ issue that I don't understand.

Code used to generate the sample:

using System.Drawing;
using System.Drawing.Imaging;

namespace LeftAlignment
{
    class Program
    {
        static void Main(string[] args)
        {
            const int LeftMargin = 10;

            // create off-screen bitmap
            using (Bitmap bitmap = new Bitmap(300, 100))
            {
                // create graphics context
                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    // clear bitmap
                    graphics.FillRectangle(Brushes.White, 0, 0, bitmap.Width, bitmap.Height);

                    // draw border and left margin
                    graphics.DrawRectangle(Pens.Gray, new Rectangle(0, 0, bitmap.Width - 1, bitmap.Height - 1));
                    graphics.DrawLine(Pens.Blue, LeftMargin, 8, LeftMargin, 92);

                    // draw string at 24 pt
                    Font font = new Font("Arial", 24);
                    graphics.DrawString("Cool water", font, Brushes.Black, LeftMargin, 8);

                    // draw string at 36 pt
                    font = new Font("Arial", 36);
                    graphics.DrawString("Cool water", font, Brushes.Black, LeftMargin, 44);
                }

                // save result as PNG
                bitmap.Save("alignment.png", ImageFormat.Png);
            }
        }
    }
}

Upvotes: 2

Views: 3577

Answers (2)

Ringo Store
Ringo Store

Reputation: 636

The story goes that Microsoft added the padding in GDI+ to make it easier to implement controls. The old GDI didn't have that problem.

When Microsoft realized it was a mistake, they added the TextRenderer class that bypasses GDI+ and uses the better GDI implementation.

The padding is supposedly 1/6 em on the left and 1/4 em on the right hand side.

You have two options:

  1. Use TextRenderer.DrawText. However, it's part of Windows Forms. So it won't be available neither in .NET Standard nor .NET Core.

  2. Use Graphics.DrawString with the magic option StringFormat.GenericTypographic. It magically removes the padding.

Also see:

Upvotes: 8

Codo
Codo

Reputation: 78825

Based on the link provided by @ChrisW, I've created an improved version (see below). It uses MeasureCharacterRanges to measure the padding added by DrawString. The result looks much butter:

Improved left alignment

As you can see, it's not perfect. There is still some white space between the blue line and the letter "C" because the measured rectangle includes the so called left-side bearing, i.e. the left side of the white space added between two characters.

So I'm still looking for an even better solution. It might be possible to compute the bearing and subtract half of the bearing in addition to the measured padding. Hopefully, it is doable with .NET Standard 2.0...

BTW: I've measured several fonts, font styles, font sizes and resolutions. It looks as if the padding is fixed. It can be computed as:

padding = 0.002312554 × font_size × resolution

(Padding in pixels, font size in pt, resolution in pixels/inch)

So as an example: for a 24pt font and graphics resolution of 96dpi, the padding would be 5.33 pixels.

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;

namespace LeftAlignment
{
    class Program
    {
        static void ImprovedDrawString(Graphics graphics, string text, Font font, float x, float y)
        {
            // measure left padding
            StringFormat sf = new StringFormat(StringFormatFlags.NoClip);
            sf.SetMeasurableCharacterRanges(new CharacterRange[] { new CharacterRange(0, 1) });
            Region[] r = graphics.MeasureCharacterRanges(text, font, new RectangleF(0, 0, 1000, 100), sf);
            float leftPadding = r[0].GetBounds(graphics).Left;

            // draw string
            sf = new StringFormat(StringFormatFlags.NoClip);
            graphics.DrawString(text, font, Brushes.Black, x - leftPadding, y, sf);
        }

        static void Main(string[] args)
        {
            const int LeftMargin = 10;
            const string Text = "Cool water";

            // create off-screen bitmap
            using (Bitmap bitmap = new Bitmap(300, 100))
            {
                // create graphics context
                using (Graphics graphics = Graphics.FromImage(bitmap))
                {
                    // enable high-quality output
                    graphics.SmoothingMode = SmoothingMode.AntiAlias;
                    graphics.TextRenderingHint = TextRenderingHint.AntiAlias;

                    // clear bitmap
                    graphics.FillRectangle(Brushes.White, 0, 0, bitmap.Width, bitmap.Height);

                    // draw border and left margin
                    graphics.DrawRectangle(Pens.Gray, new Rectangle(0, 0, bitmap.Width - 1, bitmap.Height - 1));
                    graphics.DrawLine(Pens.Blue, LeftMargin, 8, LeftMargin, 92);

                    // draw string at 24 pt
                    Font font = new Font("Arial", 24);
                    ImprovedDrawString(graphics, Text, font, LeftMargin, 8);

                    // draw string at 36 pt
                    font = new Font("Arial", 36);
                    ImprovedDrawString(graphics, Text, font, LeftMargin, 44);
                }

                // save result as PNG
                bitmap.Save("alignment.png", ImageFormat.Png);
            }
        }
    }
}

Upvotes: 2

Related Questions