My Solution to the MigraDoc forced word wrapping breaks down this way. (I should add that I know this can be optimized, but I haven't done that yet.)
Please let me know where I could clarify this explanation. I didn't proof read it very carefully yet * A New
XGraphics.MeasureString() overload that takes a Max Width parameter and an OUT parameter parameter that might be set to contains the number of characters that actually fit if the word was too long.
* A new value added to account for when WordWrap case is necessary:
FormatResult.WordWrap * Tweak
ParagraphRenderer.FormatWord(string word) and
ParagraphRenderer.MeasureString(string word) methods to use the new MeasureString method.
* Add handler for this
WordWrap case handler in the
ParagraphRenderer.Format()Here's a rough explanation that may make more sense after looking at the code:When Format() iterates over all the words (currentLeaf.Current) in the current paragraph, it calls FormatElement() which eventually calls FormatWord(), then FormatAsWord() on each word. We need to get in the middle here and break off the piece of the word that fits before it gets to FormatAsWord() when it's too long.
So,
FormatWord() measures the string, ultimately using the new XGraphics method, and if it fits,
FormatAsWord(width) does the formatting normally, otherwise, it returns the new state
FormatResult.WordWrap instead.
When
FormatResult.WordWrap is returned back to the "Format()" method, the new Case handler below picks out just the part of the word that fits, modifies the the currentLeaf to be the fitting part of the word and reTries the
FormatWord() thing again (which obviously fits). Once that part of the word got processed, it stays in this WordWrap case handler and loops over the remaining part of the word, doing the same thing, inserting new Leafs into the paragraph elements for each part of the word that fits. This looping allows, say a Chinese document with no spaces or punctuation to continuously wrap into the available area.
The new XGraphics routine measures and returns the width of the part of the word that fits the available area. If it doesn't all fit, also return how many characters do fit. What is KEY here is that the
currentLeaf.Current.Tag property is used to carry the number of fitting characters from the XGraphics measurer back to the Formatter in order for it to split off the fitting part of the word.
Here's the code: Only 2 files get modified...
In PDFsharp\code\PdfSharp\PdfSharp.Drawing\XGraphics.csCode:
/// <summary>
/// This is a special version of MeasureString that returns the width of the
/// portion of the string that fits.
/// </summary>
public XSize MeasureString(string text, XFont font, double desWidth, out int numFittingCharacters)
{
#if !GDI
throw(new System.NotSupportedException);
#endif
// Measure the Supplied String
SizeF size = gfx.MeasureString(text, font.RealizeGdiFont());
// It fits, so just return the size and indicate that everything fit.
if ((size.Width < desWidth))
{
numFittingCharacters = text.Length;
return XSize.FromSizeF(size);
}
float nWidth = size.Width;
string tempString = text;
string workString = "";
for (numFittingCharacters = text.Length; (numFittingCharacters > 0) && (nWidth > desWidth); numFittingCharacters--)
{
// Start at the end of the string and
// keep shortening until it fits.
// ----------------------------------
workString = tempString.Substring(0, numFittingCharacters);
size = gfx.MeasureString(workString, font.RealizeGdiFont());
nWidth = size.Width;
}
return XSize.FromSizeF(size);
}
The rest of the changes are In MigraDoc\code\MigraDoc.Rendering\MigraDoc.Rendering\ParagraphRenderer.csAdd
WordWrap to the existing FormatResult enum:
Code:
/// <summary>
/// Results that can occur when processing a paragraph element
/// during formatting.
/// </summary>
enum FormatResult
{
/// <summary>
/// Ignore the current element during formatting.
/// </summary>
Ignore,
/// <summary>
/// Continue with the next element within the same line.
/// </summary>
Continue,
/// <summary>
/// Start a new line from the current object on.
/// </summary>
NewLine,
/// <summary>
/// Break formatting and continue in a new area (e.g. a new page).
/// </summary>
NewArea,
/// <summary>
/// Only part of the word fit. Need to split the word at the specified
/// location and insert the remaining word after this one.
/// </summary>
WordWrap
}
Find this method:
internal override void Format(Area area, FormatInfo previousFormatInfo)Find the switch(result) statement in this method and add a new handler for this FormatResult.WordWrap case.
Code:
case FormatResult.WordWrap:
{
if (string.IsNullOrEmpty(((Text)currentLeaf.Current).Content))
Debug.WriteLine("empty string!");
if (currentLeaf.Current.Tag != null && currentLeaf.Current.Tag is int)
{
int fittingCharacters = (int)currentLeaf.Current.Tag;
currentLeaf.Current.Tag = null;
Text txtObject = currentLeaf.Current as Text;
Debug.WriteLine("CurrentWord Before: " + txtObject.Content);
if (txtObject != null)
{
string fits = txtObject.Content.Substring(0, fittingCharacters);
string remaining = txtObject.Content.Substring(fittingCharacters);
FormattedText parent = DocumentRelations.GetParentOfType(currentLeaf.Current, typeof(FormattedText)) as FormattedText;
if (parent != null)
{
int currentWordIndex = parent.Elements.IndexOf(currentLeaf.Current);
parent.Elements.InsertObject(currentWordIndex + 1, new Text(remaining));
txtObject.Content = fits;
}
}
Debug.WriteLine("CurrentWord After: " + txtObject.Content);
}
}
break;
In the Same File as above, replace the
FormatResult FormatWord(string word) method with this new version:
Code:
/// <summary>
/// Helper function for formatting word-like elements like text and fields.
/// </summary>
FormatResult FormatWord(string word)
{
XUnit width = MeasureString(word);
if (currentLeaf.Current.Tag != null)
return FormatResult.WordWrap;
if (width > 0)
return FormatAsWord(width);
else
return FormatResult.Ignore;
}
Again, In the Same File as above, replace the
XUnit MeasureString(string word) method with this:
Code:
XUnit MeasureString(string word)
{
if (string.IsNullOrEmpty(word))
return 0;
int len = word.Length;
int numFittingCharacters = 0;
XUnit width=0;
XFont xFont = CurrentFont;
// Determine how many characters of this word will fit in the supplied area.
try
{
if (formattingArea != null)
{
width = Gfx.MeasureString(word, xFont, formattingArea.Width.Point, out numFittingCharacters).Width;
if (numFittingCharacters < len && numFittingCharacters > 0)
{
// Only set this when just a portion of the word fits
currentLeaf.Current.Tag = numFittingCharacters;
}
}
else
width = Gfx.MeasureString(word, xFont, StringFormat).Width;
}
catch (Exception ex)
{
Debug.WriteLine("Null formatting area?" + ex.Message);
}
Font font = CurrentDomFont;
if (font.Subscript || font.Superscript)
width *= FontHandler.GetSubSuperScaling(xFont);
return width;
}
That should pretty much do it.