1

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);
            }
        }
    }
}
Codo
  • 75,595
  • 17
  • 168
  • 206
  • 3
    it looks like arial just has that extra space around the character. – Daniel A. White Jan 03 '19 at 15:15
  • 2
    I don't believe this is a programming problem. Characters have a certain area in which they exist including "padding" between the characters. The area is different for each character except for mono-space characters. The padding is the same regardless and increases with font size. – Terry Tyson Jan 03 '19 at 15:17
  • This can be a [pain in the *](https://www.codeproject.com/Tips/1012908/GDI-DrawString-Configurator-App) – Chris W. Jan 03 '19 at 15:36
  • @ChrisW.: Thanks you so much for this link.That explains a lot. It'll help me to properly align the text. Currently, the output looks sloppy and not professional. – Codo Jan 03 '19 at 15:42
  • and what about other, much simplier font like Verdana? – Maciej S. Jan 03 '19 at 16:07
  • Thanks for all those "just has some space...", "gets you much closer..." etc. comments. But it's just not good enough for what I'm working on. I have considerable experience in typography, in particular in the PDF area. Text and font metrics are an exact science and Windows / GDI+ can do better. – Codo Jan 03 '19 at 16:15
  • If you are aiming only at the x-axis you may be interested to know about GraphicsPath.GetBounds, which will return a pixel-perfect bounding rectangle of the path. – TaW Jan 03 '19 at 17:55

2 Answers2

7

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:

Ringo Store
  • 606
  • 3
  • 10
  • This answer saved me so much headache. I wonder how `StringFormat.GenericTypographic` sets the "no padding" flag that GDI+ respects? It really is magical. – Andy Dec 05 '22 at 03:46
2

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);
            }
        }
    }
}
Codo
  • 75,595
  • 17
  • 168
  • 206
  • I have the similar problem using Graphics.DrawString in an UserControl to show and align a TextBox. I have to use TextRender.DrawText. – qtg Dec 01 '22 at 02:06
  • Have you come up a better solution? Thanks. – qtg Dec 01 '22 at 02:07
  • 1
    I went with the `StringFormat.GenericTypographic` option proposed by *Ringo Store*. – Codo Dec 01 '22 at 07:03
  • StringFormat.GenericTypographic set NoClip, FitBlackBox and LineLimit etc but not really help for your case because StringFormat.GenericTypographic doesn't reduce the padding box. – qtg Dec 01 '22 at 09:04
  • For my use case using Graphics.DrawString() it works a treat and removes the padding. It's like magic as you wouldn't expect it from what the documentation states. – Codo Dec 01 '22 at 09:22
  • OK. It does look better I tested. Thanks. – qtg Dec 01 '22 at 09:40
  • Excuse me, how you get the formular padding = 0.002312554 × font_size × resolution? Thanks. – qtg Dec 01 '22 at 10:09
  • 1
    As far as I remember, I ran tests with MeasureCharacterRanges() for different fonts and font sizes, and then normalized it to make independent of the font and screen resolution. – Codo Dec 01 '22 at 11:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250081/discussion-between-qtg-and-codo). – qtg Dec 02 '22 at 05:42