How To Fit Text in a Rectangle

Article translations Article translations
Article ID: 200262 - View products that this article applies to.
This article was previously published under Q200262
Expand all | Collapse all

SUMMARY

Sometimes an application must create a font with a specific height and width so that a string that is drawn with the font has the appropriate horizontal extent. Typically, this is required when you want to scale, in a linear way, the vertical and the horizontal dimensions of a string that is to be drawn on a display. This article describes the considerations of fitting a string with a font to a specific two-dimensional extent.

NOTE: Before you read this article, you should be familiar with text handling terms such as cell height, character height, linear scalability, and the text handling application programming interface (API) that documented in the Platform Software Development Kit (SDK). To familiarize yourself with these terms, see the "Fonts and Text" overviews and references in the Windows GDI Platform SDK documentation.

MORE INFORMATION

TrueType fonts in Microsoft Windows operating systems are linearly scalable. However, the linearity of a TrueType font's scalability is applicable only to the character height metric of a font. All other aspects of a font's appearance rarely scale linearly with respect to the character height. This presents two problems for scaling text output when using TrueType fonts.

The first problem is that the cell height (the vertical extent of text output) does not scale linearly with the character height. This occurs because the cell height differs from the character height by an internal glyph dimension in Windows called Internal Leading. This portion of a character glyph's design is subject to TrueType hinting when scaled. Because hinting affects glyph metrics in a non-linear fashion, the cell height varies non-linearly.

This presents a problem to scaled text that has to fit in the vertical extent of a destination rectangle. If a program tries to create a series of font sizes through a continuous domain of cell heights (positive lfHeight values), a discontinuous range of actual cell heights result because the real domain of cell heights is not continuous. If a continuous domain of character sizes (negative lfHeight values) are created, the range of cell heights that result may be discontinuous.

For example, you may try to create one of each size of font with lfHeight values from 9 to 15, but the resulting cell height may be 9, 10, 11, 11, 13, 14, 15 where a cell height of 12 was requested but the result was 11. This occurs because no size of the font has cell height of 12. In this contrived example, the character sizes may be 8, 9, 10, 10, 11, 12, 13, which indicates that the request for a cell height size of 12 for the font does not have a character size equivalent.

Unfortunately, this is a characteristic of TrueType hinting that produces the readable text at screen sizes. If you must specify cell heights, you have two options:
  • Accept the closest match that the operating system chooses, and deal with the output accordingly.
  • Choose the next largest font size, and then clip the output to the destination rectangle.
If possible, programs should scale the font by using the metric that scales linearly: the character height (negative lfHeight values). By using this approach, you can get the correct linear character height. This creates text that is visually continuous and smooth scaled, but the program's code must contend with the non-linear nature of text's total extent as it scales.

The second problem is the issue of linearity in the scaling of a font's width. Generally, the smaller the font, the relatively wider the font's glyphs become. This occurs because as a font becomes smaller, you must have a relatively larger amount of horizontal space to make each glyph readable.

To make this issue even more difficult, TrueType hinting may adjust the width of an individual glyph in the font at a certain size to maintain the readability of the glyph. Consider the sum of widths of several glyphs in a string that have been adjusted by hinting. In this scenario, the width of the string is unpredictable.

If the standard TextOut function is used to output the string, smaller fonts produce text output that extend beyond the required horizontal extent that has been linearly scaled. Larger fonts cause the text output to fall short of the horizontal requirement. There are two solutions to this problem:
  • Make the font larger or smaller until the horizontal extent comes close to the requirement. This approach suffers from two flaws in that you will unlikely get the exact horizontal extent, and the vertical scale requirement is broken.
  • Use the font size that meets the vertical requirement, but do not use TextOut. Use ExtTextOut instead to position all of the characters of the string to cover the horizontal extent of the rectangle.
The second approach is demonstrated in the sample code that follows. The only major drawback is that readability of the text suffers a little because the character spacing has been changed from the character spacing that is designed into the font.
BOOL GetTextBlackExtentPoint(
  HDC hdc,           // handle to DC
  LPCTSTR lpString,  // text string
  int cbString,      // number of characters in string
  LPSIZE lpSize      // string size
)
{
    SIZE sizeTx;
    ABC lead;
    ABC trail;
    TCHAR ch = lpString[lstrlen(lpString)-1];

    if (NULL == lpSize)
        return false;

    if (!GetTextExtentPoint32(hdc, lpString, cbString, &sizeTx))
        return false;

    if (!GetCharABCWidths(hdc, lpString[0], lpString[0], &lead))
        return false;
    if (!GetCharABCWidths(hdc, ch, ch, &trail))
        return false;
    sizeTx.cx = sizeTx.cx - lead.abcA -trail.abcC;
    *lpSize = sizeTx;
    return true;
}


BOOL SnugTextOut( HDC hDC, RECT &rc, LPTSTR szString )
{
    int     i, nStringLength;
    BOOL    bResult;
    int     *pDx;
    int     nX = rc.left;
    int     nY = rc.top;
    int     Width = 0;

    // How long is the string - you need this later in this code.
    nStringLength = lstrlen( szString );

    // Allocate enough memory for the intercharacter spacing array.
    pDx = (int *)new int[ sizeof(int) * nStringLength ];

    // Initialize the array with the standard values.
    for(i=0; i < nStringLength; i++)
    {
        ABC     abc;
        if( ! GetCharABCWidths( hDC, szString[i], szString[i], &abc ) )
        {
            delete [] pDx;
            return FALSE;
        }
        pDx[i] = abc.abcA + abc.abcB + abc.abcC;
        
        // You need the width.
        Width += pDx[i];
        
        // Also, account for the Black extent of the string.
        if (i == 0)
        {
            // Adjustment before the first character for underhang
            nX -= abc.abcA;
            Width -= abc.abcA;
        }
        if (i == nStringLength-1)
        {
            // Adjustment for overhang
            Width -= abc.abcC;
        }

    }

    int deltaCX = rc.right-rc.left - Width;
    int deltaCh = deltaCX / nStringLength;
    int remainder = deltaCX % nStringLength;
    int error = 0;

    // Distribute the adjustment through the intercharacter spacing.
    // For a more typographically correct approach, distribute the 
    // adjustment in the "white space."
    for(i=0; i < nStringLength; i++)
    {
        pDx[i] += deltaCh;
        error += remainder;
        if (abs(error) >= nStringLength)    // adjustment?
        {
            int adjustment = abs(error)/error;
            pDx[i] += adjustment;
            error -= nStringLength*adjustment;
        }

    }

    // ExtTextOut() draws our text with our ICS array.
    bResult = ExtTextOut( hDC, nX, nY, 0, &rc, szString, nStringLength, pDx );

    // Clean up.
    delete [] pDx;
    return bResult;
}

double ScaleFactor = 1.0;

BOOL OnPaint(HWND hWnd)
{
    PAINTSTRUCT     ps;
    HDC             hdc;
    LOGFONT         lf;
    HFONT           hFont, hScaledFont, hOldFont;
    SIZE            sizeText;
    TCHAR           Buffer[] = TEXT("for this is sample text of");
    RECT            rcText;

    hdc = BeginPaint(hWnd, &ps);

    // Get the unscaled metrics.
    ZeroMemory(&lf, sizeof(lf));
    lf.lfHeight = -MulDiv( 14, GetDeviceCaps(hdc, LOGPIXELSY), 72 );
    lf.lfCharSet = ANSI_CHARSET;
    lf.lfItalic = true;
    lstrcpy(lf.lfFaceName, TEXT("Times New Roman"));
    hFont = CreateFontIndirect(&lf);
    hOldFont = (HFONT)SelectObject(hdc, hFont);

    GetTextBlackExtentPoint(hdc, Buffer, lstrlen(Buffer), &sizeText);
    SelectObject(hdc, hOldFont);


    // Draw the scaled text.
    lf.lfHeight = -MulDiv( 14*ScaleFactor*100, GetDeviceCaps(hdc, LOGPIXELSY), 7200 );
    lf.lfCharSet = ANSI_CHARSET;
    lf.lfItalic = true;
    lstrcpy(lf.lfFaceName, TEXT("Times New Roman"));
    hScaledFont = CreateFontIndirect(&lf);

    SelectObject(hdc, hScaledFont);

    rcText.left = 100;
    rcText.top = 100;
    rcText.right = rcText.left + sizeText.cx*ScaleFactor;
    rcText.bottom = rcText.top + sizeText.cy*ScaleFactor;

    SnugTextOut(hdc, rcText, Buffer);

    SelectObject(hdc, GetStockObject(NULL_BRUSH));

    // Rectangles are right-bottom exclusive.
    rcText.right++;
    rcText.bottom++;
    Rectangle(hdc, rcText.left, rcText.top, rcText.right, rcText.bottom);

    // Clean up.
    SelectObject(hdc, hOldFont);
    DeleteObject(hFont);
    DeleteObject(hScaledFont);

    EndPaint(hWnd, &ps);
    return true;
}
				
To see this sample code in action, create a simple Win32 Windows application, and use the OnPaint function to process the WM_PAINT message. For some user interaction such as a keystroke, a menu item selection, or a mouse click, change the ScaleFactor variable, and then invalidate the client area.

REFERENCES

For more information, see the following Windows GDI Platform SDK documentation:
Fonts and Text
http://msdn.microsoft.com/en-us/library/ms534210.aspx

Properties

Article ID: 200262 - Last Review: September 22, 2011 - Revision: 4.0
APPLIES TO
  • Microsoft Win32 Application Programming Interface, when used with:
    • Microsoft Windows Millennium Edition
    • Microsoft Windows 98 Standard Edition
Keywords: 
kbdswgdi2003swept kbfont kbgdi kbhowto KB200262

Give Feedback

 

Contact us for more help

Contact us for more help
Connect with Answer Desk for expert help.
Get more support from smallbusiness.support.microsoft.com