Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GDI+ drawing text with different sizes on a baseline has off-by-1px problems

Tags:

c#

.net

gdi+

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.

like image 453
Dilum Ranatunga Avatar asked Feb 01 '12 17:02

Dilum Ranatunga


2 Answers

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

like image 164
LarsTech Avatar answered Sep 26 '22 02:09

LarsTech


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.

like image 27
Coincoin Avatar answered Sep 26 '22 02:09

Coincoin