Calculate text height based on available width and font?
Asked Answered
Y

9

17

We are creating PDF documents on the fly from the database using PDFsharp.

I need to know the best way to calculate the height of the text area based on the font used and the available width.

I need to know the height so I can process page breaks when required.

Ynez answered 23/5, 2009 at 10:53 Comment(0)
A
10

In .NET you can call Graphics.MeasureString to find out how large the drawn text is going to be.

Right, but when using PDFsharp you call XGraphics.MeasureString.

Accordingly answered 25/8, 2009 at 11:7 Comment(2)
This doesn't seem like the answer. MeasureString does not take rect/width and so cannot know the drawn height - or have I missed something?Tidy
Use XGraphics.MeasureString to find the dimensions of a single line of text. See XTextFormatter class for code that breaks lines automatically (by using MeasureString()). It should be easy to add a new method XTextFormatter.MeasureString that returns the height. We normally use MigraDoc in our application so we only add text to the document and MigraDoc cares for linebreaks and pagebreaks.Accordingly
H
21

The PdfSharp.Drawing.XGraphics object has a MeasureString method that returns what you require.

 var pdfDoc = new PdfSharp.Pdf.PdfDocument();
 var pdfPage = pdfDoc.AddPage();
 var pdfGfx = PdfSharp.Drawing.XGraphics.FromPdfPage(pdfPage);
 var pdfFont = new PdfSharp.Drawing.XFont("Helvetica", 20);

 while (pdfGfx.MeasureString("Hello World!").Width > pdfPage.Width)
      --pdfFont.Size;

 pdfGfx.DrawString("Hello World!", pdfFont
      , PdfSharp.Drawing.XBrushes.Black
      , new PdfSharp.Drawing.XPoint(100, 100));

This should help you. Please consider that I didn't test this code as I wrote it on the fly in order to help. It might contain some compile-time errors, but you may get the idea.

Homo answered 24/8, 2009 at 16:16 Comment(2)
what is the class "PdfSharp"?Acidhead
@Chanipoz: This not not actually a class, this is a PDF n-tier application to help the production of PDF files on the fly on code-side.Homo
A
10

In .NET you can call Graphics.MeasureString to find out how large the drawn text is going to be.

Right, but when using PDFsharp you call XGraphics.MeasureString.

Accordingly answered 25/8, 2009 at 11:7 Comment(2)
This doesn't seem like the answer. MeasureString does not take rect/width and so cannot know the drawn height - or have I missed something?Tidy
Use XGraphics.MeasureString to find the dimensions of a single line of text. See XTextFormatter class for code that breaks lines automatically (by using MeasureString()). It should be easy to add a new method XTextFormatter.MeasureString that returns the height. We normally use MigraDoc in our application so we only add text to the document and MigraDoc cares for linebreaks and pagebreaks.Accordingly
B
7

I had a similiar problem so I implemented this extension method:

public static double MeasureHeight(this PdfSharp.Drawing.XGraphics gfx, string text, PdfSharp.Drawing.XFont font, int width)
{
    var lines = text.Split('\n');

    double totalHeight = 0;

    foreach (string line in lines)
    {
        var size = gfx.MeasureString(line, font);
        double height = size.Height + (size.Height * Math.Floor(size.Width / width));

        totalHeight += height;
    }

    return totalHeight;
}
Buran answered 3/4, 2014 at 11:42 Comment(1)
This worked well for me as a quick and simple solution. I did notice however, when doing the size.Height * Math.Floor(size.Width / width), it will sometimes underestimate the width (or maybe the TextFormatter overestimates the width when wrapping text). So I had to increase the width value by about 15% to make it more accurate. Not sure why, if anyone could explain that would be helpful.Antonia
A
5

In .NET you can call Graphics.MeasureString to find out how large the drawn text is going to be.

Arita answered 24/5, 2009 at 5:56 Comment(0)
L
2

I wrote a small extension method to the XGraphic object to do just that : Calclulate the exact text height (and width) by specifiying the maxWidth. See the following gist for the code : https://gist.github.com/erichillah/d198f4a1c9e8f7df0739b955b245512a

Laxative answered 11/10, 2016 at 10:1 Comment(0)
G
0

The OP asked how to calculate text height based on available width and font. Windows .NET provides an API call for this which takes a width argument; the version of PDFsharp I'm using (0.9.653, .NET 1.1) does not.

My solution - use the .NET API call with a Graphics object allocated for a custom-created Bitmap object to get the answer.

What worked for me was to use a Bitmap that had 100 DPI resolution (critical) and happened to be the size of a Portrait page (probably less critical).

Then I just asked .NET what the pixel size would be for painting on that bitmap.

You probably will then want to convert the units from 1/100th of an inch to Points (for PDFsharp).

''' Adapted Code - this not tested or even compiled - Caveat Emptor!
''' Target: Visual Basic, .NET 1.1 (VS2003) [adapt as necessary]

'  '  '  '  '  '  '  '  '  '  '  '  '  '  '  '
'  GraphicsAlt.MeasureString() does substantially what System.Drawing MeasureString(...,Integer) does.
'  '  '  '  '  '  '  '  '  '  '  '  '  '  '  '
Public Module GraphicsAlt

    '
    ' Static data used Only to compute MeasureString() below.
    '
    '     Cache a single copy of these two objects, to address an otherwise unexplained intermittent exception.
    '
    Private Shared myImage As Bitmap = Nothing
    Private Shared myGraphics As Graphics = Nothing

    Public Shared Function GetMeasureGraphics() As Graphics
        If myImage Is Nothing Then
            myImage = New Bitmap(1700, 2200)  '' ... Specify 8.5x11 
            myImage.SetResolution(100, 100)   '' ... and 100 DPI (if you want different units, you might change this)
            myGraphics = Graphics.FromImage(myImage)
        End If
        Return myGraphics
    End Function

    'Given 1/100TH inch max width, return Rect to hold with units 1/100TH inch
    '
    Public Function MeasureString(ByVal text As String, ByVal aFont As System.Drawing.Font, ByVal width As Integer) As System.Drawing.SizeF
        Return (GraphicsAlt.GetMeasureGraphics()).MeasureString(text, aFont, width)
    End Function

End Module
Glaucoma answered 7/3, 2013 at 16:40 Comment(1)
That sounds like a hack as the results may differ from XGraphics.MeasureString used by PDFsharp/MigraDoc internally due to rounding errors, kerning, different treatment of bold and italics.Accordingly
M
0

In case anyone still wants to find an answer, I've implemented a reasonably easy-to-understand method to find out the height of the resulting text.

Public Function PrintString(text As String, ft As XFont, rect As XRect, graph As XGraphics, b As SolidBrush, Optional tf As XTextFormatter = Nothing) As Integer
    If Not IsNothing(tf) Then
        tf.DrawString(text, ft, b, rect)
    Else
        Dim drawLeft As New XStringFormat
        drawLeft.Alignment = XStringAlignment.Near

        graph.DrawString(text, ft, b, rect, drawLeft)
    End If

    Dim width As Double = graph.MeasureString(text, ft).Width
    Dim multiplier As Integer = 0

    While width > 0
        multiplier += 1

        width -= rect.Width
    End While

    Dim height As Double = (graph.MeasureString(text, ft).Height) * multiplier
    Return height
End Function

Explaining the code:

First, print the text. I included an Optional XTextFormatter called tf because I use either XGraphics or XTextFormatters interchangeably in my application.

Then, calculate how long the text was by MeasureString().Width.

Then, calculate how many lines of text there were. This is done by dividing the total length of the text found earlier by the width of the provided rectangle (box) where the tax is printed. I did it with a while loop here.

Multiply the height of the text (using graph.MeasureString().Height) by the number of lines there were. This is the final height of your text.

Return the height value. Now, calling the PrintString() function will print the text provided out while returning the height of the printed text afterward.

Marylou answered 18/6, 2015 at 9:26 Comment(3)
The code does not take into account that XTextFormatter adds line breaks between words only. The result of PrintString may be off by one or even more lines. The source code for the XTextFormatter class is included with PDFsharp. This class is meant to get people started and you can add page breaks or an out parameter that returns the height. The While loop can be replaced by a division (but this won't improve the results).Accordingly
@PDFsharpTeam I would dearly like to know how to do that. I've always wondered why the XTextFormatter class doesn't automatically return the height of the resulting text. How does one edit the class?Marylou
@Marylou I created an XTextFormatterEx class based on XTextFormatter. It was easy to edit the class, but I had some technical problems to get it to compile. The complete source code can be found in the PDFsharp forum. See my answer to this question.Accordingly
A
0

PDFsharp includes a class XTextFormatter that can be used to draw text with linebreaks.

However it can not determine the height needed for the text. Inspired by a comment from @Wakka02 I improved this class, generating class XTextFormatterEx.
In my opinion it also answers the original question, therefore I post an answer.
I know this is an old question and the answer may not help the OP, but it is a frequently asked question and the answer may help others.

The new class has 500 lines of code - and I think this would be too much for this post.

The source code can be found on the PDFsharp forum:
http://forum.pdfsharp.net/viewtopic.php?p=9213#p9213

It can also be found in my humble blog:
http://developer.th-soft.com/developer/pdfsharp-improving-the-xtextformatter-class-measuring-the-height-of-the-text/

When using the new class, you can first call PrepareDrawString to find out how much of the text fits and which height the fitting text has. Then your decoder can draw the prepared text or prepare another text or prepare the same text with a different rectangle.

My new class at work: XTextFormatterEx tf = new XTextFormatterEx(gfx); int lastCharIndex; double neededHeight;

// Draw the text in a box with the optimal height
// (magic: we know that one page is enough).
XRect rect = new XRect(40, 100, 250, double.MaxValue);
//tf.Alignment = ParagraphAlignment.Left;
tf.PrepareDrawString(text, font, rect,
                     out lastCharIndex, out neededHeight);
rect = new XRect(40, 100, 250, neededHeight);
gfx.DrawRectangle(XBrushes.SeaShell, rect);
// Both variants should look the same.

// Optimized version: draw the prepared string.
tf.DrawString(XBrushes.Black, XStringFormats.TopLeft);

Preparing the text invokes MeasureString many times. Later the prepared text can be drawn without invoking MeasureString again.

As of today (Juli 17, 2015) the class XTextFormatterEx (like the original XTextFormatter) uses internal fields of the XFont class. This requires special treatment when compiling the class. I decided to copy my XTextFormatterEx class into the PDFsharp folder after downloading the complete source package for PDFsharp 1.32.
Anybody trying to modify either the XTextFormatter or XTextFormatterEx class will face the same problem.
I hope this issue will be solved with future versions of PDFsharp, allowing modified versions of these classes to be included in the application project.

Accordingly answered 17/7, 2015 at 20:15 Comment(0)
C
0

Here's an extension to PdfSharp's XGraphics to measure the height of a box of text with line breaks. It was inspired by Eric-H's answer above, but takes into account the line spacing rules of the paragraph, and also implements a more accurate text wrapping logic.

// Enhanced version of MeasureString function for PdfSharp XGraphics taking to account line breaks within a restricted width (maxWidth)
// The returned height will be the font's Content height - from the Ascend of the first line to the Descend of the last line, i.e.
// the height will include the necessary spacing between the lines, but not the usual spacing under the last line.
// The returned width will be less than maxWidth if the text could be wrapped, but it can be greater than maxWidth
// if the text contained a word that could did not fit and so could not be wrapped
public static XSize MeasureWrappedText(
    this XGraphics gfx,
    string text,
    ParagraphFormat format,
    Unit maxWidth,
    out int lineCount
)
{
    XFont font = new XFont(format.Font.Name, format.Font.Size, GetXStyle(format.Font));
    XSize space = gfx.MeasureString(" ", font);

    // Wrap the text by line breaks, white-spaces and hyphens and count the resulting lines
    // Note that this logic may not be exactly what PdfSharp's renderer implements so a discrepancy may occur in some rare cases
    double width = 0;
    double lineWidth = 0;
    lineCount = 0;
    var p = new (string Word, double WordWidth, double Width, bool IsSpace, double LineWidth)[3]; // to remember 3 previous steps
    foreach (string word in SplitIntoWordsAndSpaces(text))
    {
        double wordWidth = word == " " ? space.Width : gfx.MeasureString(word, font).Width;
        bool isNewLine = word is "\n" or "\r" or "\r\n";
        bool isSpace = word.Length == 1 && char.IsWhiteSpace(word[0]);
        lineWidth += wordWidth;
        if (isNewLine || lineWidth > maxWidth && maxWidth > space.Width) // if our maxWidth is smaller than the space width (e.g. 0), we don't wrap at all
        {
            lineWidth = isSpace || isNewLine ? 0 : wordWidth; // if we are breaking at a white-space or a new line, forget its width
            int previous = 0;
            if (p[0].IsSpace)
                width = p[1].Width; // roll-back any width increase due to the space before this word
            else if (word == "-" && p[0].Word != "-") // a special case for a hyphen - we break after it, not before
            {
                lineWidth += p[0].WordWidth; // keep the hyphen together with the previous word
                width = p[p[1].IsSpace ? 2 : 1].Width; // roll-back any width increase due to our word followed by the hyphen
                previous = 1;
            }
            if (isNewLine || p[previous].LineWidth > 0)
                lineCount++;
        }
        width = Math.Max(width, lineWidth);
        p[2] = p[1];
        p[1] = p[0];
        p[0] = (word, wordWidth, width, isSpace, lineWidth);
    }
    if (lineWidth > 0)
        lineCount++;

    if (lineCount > 1)
    {
        double singleLineSpace = font.GetHeight();
        double lineHeight = format.LineSpacingRule switch
        {
            LineSpacingRule.Single => singleLineSpace,
            LineSpacingRule.OnePtFive => 1.5 * singleLineSpace,
            LineSpacingRule.Double => 2.0 * singleLineSpace,
            LineSpacingRule.Multiple => format.LineSpacing * singleLineSpace,
            LineSpacingRule.AtLeast => Math.Max(singleLineSpace, format.LineSpacing),
            LineSpacingRule.Exactly => format.LineSpacing,
            _ => 0
        };
        return new XSize(width, space.Height + (lineCount - 1) * lineHeight);
    }

    return new XSize(width, space.Height);
}

private static IEnumerable<string> SplitIntoWordsAndSpaces(string s)
{
    int i = 0;
    int j = 0;
    while (j < s.Length)
    {
        if (char.IsWhiteSpace(s[j]) || s[j] == '-')
        {
            int len = j < s.Length - 1 && s.Substring(j, 2) == "\r\n" ? 2 : 1;
            if (i < j)
                yield return s[i..j];
            yield return s.Substring(j, len);
            j += len;
            i = j;
        }
        else
            j++;
    }
    if (i < j)
        yield return s[i..j];
}
Change answered 8/4 at 10:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.