1

I am trying to measure the size of a string given a certain font using the TextRenderer class. Despite the fact that i tried measuring it with 3 different approaches (Graphics.MeasureCharacterRanges, Graphics.MeasureString, TextRenderer.MeasureText) and they all give me different results without being accurate, i've stumbled across something else.
Measuring the same string START with the same font using a fontsize of 7 and 8, the fontsize 7 measurement turns out to be wider than the the fontsize 8 measurement.

Here's the code i use:

Font f1 = new Font("Arial", 7, FontStyle.Regular);
Font f2 = new Font("Arial", 8, FontStyle.Regular);
Size s1 = TextRenderer.MeasureText("START", f1);
Size s2 = TextRenderer.MeasureText("START", f2);

The result is s1 having a width of 41 and a height of 13 while s2 having a width of 40 and a height of 14.

Why would a smaller font result in a larger width?

dm1988
  • 87
  • 2
  • 11
  • Hm, I can't reproduce. Here I get 44,15 and 53,16 .Try SizeF instead! My monitor has 120dpi. – TaW Apr 14 '16 at 13:22
  • Note that all except Graphics.MeasureString with StringFormat.TypographicGenic add some whitespace to allow you to string the output together without overlapping the words. – TaW Apr 14 '16 at 13:29
  • @TaW my monitor is running on the default 96dpi. TextRenderer doesn't offer any methods that would measure a string resulting in a SizeF. I don't mind the size being larger due to additonal padding, i was just wondering how a smaller font size results in larger width. – dm1988 Apr 14 '16 at 13:32
  • 1
    It is a 96 dpi measurement. And correct, 8 point text is actually wider than 7 point at 96 dpi. You'll have to get used to the mysteries and inaccuracies of measuring text, TrueType hinting, pixel-grid fitting and typographic features like glyph overhang, ligatures and kerning make it an inexact art. – Hans Passant Apr 14 '16 at 13:32

2 Answers2

3

To address specifically why it would be possible for a larger font to produce a smaller width, I put together this sample console app. It's worth noting that I adjust the 7 & 8 font sizes to 7.5 & 8.25, respectively, as this is what size TextRenderer evaluates them as internally.

using System;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;

namespace FontSizeDifference
{
    static class Program
    {
        [StructLayout(LayoutKind.Sequential)]
        struct ABCFLOAT
        {
            public float abcfA;
            public float abcfB;
            public float abcfC;
        }

        [DllImport("gdi32.dll")]
        static extern bool GetCharABCWidthsFloat(IntPtr hdc, int iFirstChar, int iLastChar, [Out] ABCFLOAT[] lpABCF);

        [DllImport("gdi32.dll", CharSet = CharSet.Auto, EntryPoint = "SelectObject", SetLastError = true)]
        static extern IntPtr SelectObject(IntPtr hdc, IntPtr obj);

        [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        static extern bool DeleteObject([In] IntPtr hObject);

        [StructLayout(LayoutKind.Sequential)]
        struct KERNINGPAIR
        {
            public ushort wFirst;
            public ushort wSecond;
            public int iKernAmount;
        }

        [DllImport("gdi32.dll")]
        static extern int GetKerningPairs(IntPtr hdc, int nNumPairs, [Out] KERNINGPAIR[] lpkrnpair);

        [STAThread]
        static void Main()
        {
            var fonts = new[] {
                new Font("Arial", 7.5f, FontStyle.Regular),
                new Font("Arial", 8.25f, FontStyle.Regular)
            };
            string textToMeasure = "START";

            using (Graphics g = Graphics.FromHwnd(IntPtr.Zero))
            {
                IntPtr hDC = g.GetHdc();

                foreach (Font font in fonts)
                {
                    float totalWidth = 0F;
                    IntPtr hFont = font.ToHfont();

                    // Apply the font to dc
                    SelectObject(hDC, hFont);

                    int pairCount = GetKerningPairs(hDC, short.MaxValue, null);
                    var lpkrnpair = new KERNINGPAIR[pairCount];
                    GetKerningPairs(hDC, pairCount, lpkrnpair);

                    Console.WriteLine("\r\n" + font.ToString());

                    for (int ubound = textToMeasure.Length - 1, i = 0; i <= ubound; ++i)
                    {
                        char c = textToMeasure[i];
                        ABCFLOAT characterWidths = GetCharacterWidths(hDC, c);
                        float charWidth = (characterWidths.abcfA + characterWidths.abcfB + characterWidths.abcfC);
                        totalWidth += charWidth;

                        int kerning = 0;
                        if (i < ubound)
                        {
                            kerning = GetKerningBetweenCharacters(lpkrnpair, c, textToMeasure[i + 1]).iKernAmount;
                            totalWidth += kerning;
                        }

                        Console.WriteLine(c + ": " + (charWidth + kerning) + " (" + charWidth + " + " + kerning + ")");
                    }

                    Console.WriteLine("Total width: " + totalWidth);

                    DeleteObject(hFont);
                }

                g.ReleaseHdc(hDC);
            }
        }

        static KERNINGPAIR GetKerningBetweenCharacters(KERNINGPAIR[] lpkrnpair, char first, char second)
        {
            return lpkrnpair.Where(x => (x.wFirst == first) && (x.wSecond == second)).FirstOrDefault();
        }

        static ABCFLOAT GetCharacterWidths(IntPtr hDC, char character)
        {
            ABCFLOAT[] values = new ABCFLOAT[1];
            GetCharABCWidthsFloat(hDC, character, character, values);
            return values[0];
        }
    }
}

For each font size, it outputs the width of each character, including kerning. At 96 DPI, for me, this results in:

[Font: Name=Arial, Size=7.5, Units=3, GdiCharSet=1, GdiVerticalFont=False]
S: 7 (7 + 0)
T: 6 (7 + -1)
A: 7 (7 + 0)
R: 7 (7 + 0)
T: 7 (7 + 0)
Total width: 34

[Font: Name=Arial, Size=8.25, Units=3, GdiCharSet=1, GdiVerticalFont=False]
S: 7 (7 + 0)
T: 5 (6 + -1)
A: 8 (8 + 0)
R: 7 (7 + 0)
T: 6 (6 + 0)
Total width: 33

Though I've obviously not captured the exact formula for measurements made by TextRenderer, it does illustrate the same width-discrepancy. At font size 7, all characters are 7 in width. However, at font size 8, the character widths begin to vary, some larger, some smaller, ultimately adding up to a smaller width.

cokeman19
  • 2,405
  • 1
  • 25
  • 40
1

It seems like TextRenderer.MeasureText is giving correct value, however smaller font has bigger letter-spacing for some glyphs.

Below you can see how it looks for "TTTTTTTTT" text. Upper one is Arial 7, bottom one is Arial 8.

For bigger font there is no space between letters.

enter image description here

lukbl
  • 1,763
  • 1
  • 9
  • 13