10

I need to print numbers where some of the digits in the middle are emphasized by increasing the font size and weight. In the example below, 456 is emphasized.

enter image description here

The font and the two sizes used are user-configurable.

The current code does this using three calls to Graphics.DrawString(...).

The problem I am having is that with most fonts, I am seeing off-by-1pixel problems (relative to the gray line, the 456 is sitting an extra pixel higher that the other digits):

enter image description here

I've attached some debugging dump (of the Bob Powell formula) for various fonts at the bottom of my post. The other techniques yielded similar results.

In order to print text on a common baseline, one needs to calculate the baseline offset for a particular Font. I've tried using three techniques:

First, MSDN's code: http://msdn.microsoft.com/en-us/library/xwf9s90b(v=vs.80).aspx

ascent = fontFamily.GetCellAscent(FontStyle.Regular);
ascentPixel = font.Size * ascent / fontFamily.GetEmHeight(FontStyle.Regular)

Second, code from: Using GDI+, what's the easiest approach to align text (drawn in several different fonts) along a common baseline?

Finally, code from Bob Powell's post: Formatting text on a common baseline.

Here's my draw method:

private void DrawOnBaseline(Graphics g, string text, FontWithBaseline fwb, Brush brush, float x, float y) {
  g.DrawString(text, fwb.Font, brush, x, y - fwb.Baseline, StringFormat.GenericTypographic);
}

Where FontWithBaseline simply associates a Font with its respective baseline calculation:

public class FontWithBaseline {
  private Font m_font;
  private float m_baseline;

  public FontWithBaseline(Font font) {
    m_font = font;
    m_baseline = CalculateBaseline(font);
  }

  public Font Font { get { return m_font; } }
  public float Baseline { get { return m_baseline; } }

  private static float CalculateBaseline(Font font) {
    ... // I've tried the three formulae here.
  }
}

I have not experimented with Graphics.TestRenderingHint yet. Is that the magic sauce? What am I missing? Is there an alternative API I can use, where I call the call to draw supplies the baseline's Y coordinate?

enter image description here


Update 1

I interpolated my code with @LarsTech. He was doing one subtly different; he was adding a 0.5f. However, even this variant does not fix the issue. Here's the code:

protected override void OnPaint(PaintEventArgs e) {
  base.OnPaint(e);
  TryLarsTechnique(e);
}

private void TryLarsTechnique(PaintEventArgs e) {
  base.OnPaint(e);
  Graphics g = e.Graphics;
  GraphicsContainer c = g.BeginContainer();
  g.Clear(Color.White);
  g.SmoothingMode = SmoothingMode.AntiAlias;
  g.TextRenderingHint = TextRenderingHint.AntiAlias;
  Font small = new Font("Arial", 13, FontStyle.Regular, GraphicsUnit.Pixel);
  Font large = new Font("Arial", 17, FontStyle.Bold, GraphicsUnit.Pixel);

  int x = 100;
  int y = 100;
  x += DrawLars(g, "12.3", small, x, y);
  x += DrawLars(g, "456", large, x, y);
  x += DrawLars(g, "8", small, x, y);
  g.EndContainer(c);
}

// returns width of text
private int DrawLars(Graphics g, string text, Font font, int x, int y) {
  float offset = font.SizeInPoints /
                 font.FontFamily.GetEmHeight(font.Style) *
                 font.FontFamily.GetCellAscent(font.Style);
  float pixels = g.DpiY / 72f * offset;
  int numTop = y - (int)(pixels + 0.5f);      
  TextRenderer.DrawText(g, text, font, new Point(x, numTop), Color.Black, Color.Empty, TextFormatFlags.NoPadding);
  return TextRenderer.MeasureText(g, text, font, Size.Empty, TextFormatFlags.NoPadding).Width;
}

I am wondering whether specifying font size using GraphicsUnit.Pixel is the culprit. Perhaps there is a way to find on the preferred sizes for any particular font?


Update 2

I've tried to specify font sizes in Points instead of Pixels, and this doesn't fully solve the problem, either. Note that using just whole point sizes is not an option in my case. To see if this is possible, I tried this on Windows Wordpad. Sizes Using 96 dpi (and 72 points per inch by definition), 17px, 13px translate to 12.75 and 9.75. Here's the output compared:

enter image description here

Notice how the smaller fonts are the same height at a pixel level. So Wordpad somehow manages to get this correct without rounding the font sizes to convenient values.

Andrew Morton
  • 24,203
  • 9
  • 60
  • 84
Dilum Ranatunga
  • 13,254
  • 3
  • 41
  • 52

3 Answers3

5

You haven't shown enough code to reproduce the problem, so here is a working example using that Bob Powell example you gave.

Demonstration code only:

private void panel1_Paint(object sender, PaintEventArgs e) {
  e.Graphics.Clear(Color.White);
  e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
  e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;

  string[] numbers = new string[] { "1", "2", ".", "3", "4", "5", "6", "8" };

  int x = 10;
  int y = 30;

  foreach (string num in numbers) {
    Font testFont;
    if (num == "4" || num == "5" || num == "6")
      testFont = new Font("Arial", 16, FontStyle.Bold);
    else
      testFont = new Font("Arial", 11, FontStyle.Regular);

    float offset = testFont.SizeInPoints / 
                   testFont.FontFamily.GetEmHeight(testFont.Style) * 
                   testFont.FontFamily.GetCellAscent(testFont.Style);
    float pixels = e.Graphics.DpiY / 72f * offset;

    int numTop = y - (int)(pixels + 0.5f);

    TextRenderer.DrawText(e.Graphics, num, testFont, new Point(x, numTop), 
                          Color.Black, Color.Empty, TextFormatFlags.NoPadding);

    x += TextRenderer.MeasureText(e.Graphics, num, testFont, 
                                  Size.Empty, TextFormatFlags.NoPadding).Width;
  }

  e.Graphics.DrawLine(Pens.Red, new Point(5, y + 1), new Point(x + 5, y + 1));
}

This produces:

enter image description here

LarsTech
  • 80,625
  • 14
  • 153
  • 225
  • Thanks for this. I've updated my question to add my code and incorporate your approach. The addition of `0.5f` was something new. Alas, I still see the problem. Try using font sizes 17 pixels + 13 pixels; I suspect you will see the problem too. – Dilum Ranatunga Feb 01 '12 at 19:14
  • @DilumRanatunga The `0.5f` came straight from Bob Powell's example (see bottom of that linked page). Yes, using `GraphicsUnit.Pixel` nudges it up a pixel. Not sure why, but leaving it off drew them back in line again. I believe `GraphicsUnit.Point` is the default, and when using that, it worked, too. – LarsTech Feb 01 '12 at 19:29
  • @DilumRanatunga If I change the code to GraphicsUnit.Pixel and then use `12.75f` and `9.75` as my font size, it lines up correctly. I don't *think* WorkPad is doing any rounding, I think it's just showing a whole number to the user for convenience. I might be wrong. The Visual Studio designer shows `8.25pt` for the default `Microsoft Sans Serif` font, but if you click on the dialog box to change the font, you only see whole numbers in the drop down. Not sure if that helps you or not. – LarsTech Feb 01 '12 at 21:06
2

Maybe the problem is that you are specifying your font size in pixels while your offset is calculated in points and converted back in pixels. This might introduce all sorts of imprecisions.

Try specifying the font sizes in points and see if it works.

Coincoin
  • 27,880
  • 7
  • 55
  • 76
  • I agree that a rounding issue could well be the underlying cause. But I need to address that at a programmatic level. See my second update. Windows WordPad manages to make this all work... – Dilum Ranatunga Feb 01 '12 at 20:09
  • Surely you tried setting the font size in points? What were your results? I'm asking because it could be a clue about what is going on. – Coincoin Feb 02 '12 at 16:58
  • I did try using Points. For certain font sizes, it works. The problem is that I cannot restrict my solution to those font sizes. My control needs to support the user specified sizes. It also needs to support proportionally scaling the sizes down when the number contains too many digits. If there were a programmatic way to find 'good' sizes, and there were many such sizes, I could adopt such an approach. – Dilum Ranatunga Feb 02 '12 at 17:30
  • `TextRenderer` is very agressive in hinting and might be forcing a font size different than the one returned in SizeInPoints so that everything gets hinted correctly. Next step would be to try to use the `Graphics` text renderer instead of the `TextRenderer` and see if the same behaviour happens. – Coincoin Feb 03 '12 at 05:52
1

When Graphics.TextRenderingHint is set to use grid fitting, the actual metrics of a scaled TrueType font in pixels are determined by GDI+ through searching font's VDMX table, rather than by simply scaling and rounding its design metrics. That's what I figured from stepping through Graphics.DrawString in disassembly.

Warp
  • 11
  • 1