0

I've noticed on Windows 8.1 and newer operating systems that the spacing between glyphs is incorrect when drawing text using GraphicsPath.AddString().

The issue is extra obvious on fonts like Script MT Bold and other script fonts. You can see that the line between the characters doesn't properly connect to each other.

I've tried calling AddString() with different StringFormat configurations with no success. I see this behaviour both when using a PrivateFontCollection and normal fonts.

For our use case it is absolutely essential that we keep using a GraphicsPath object.

TextRender.DrawText and WPF behaves correctly.

I suspect this is a bug in GDI+.

I'm looking for a workaround or a way to make GraphicsPath.AddString() start behaving correctly.

We are also using PrivateFontCollection.

Per Hyyrynen
  • 81
  • 1
  • 14
  • might be related to https://stackoverflow.com/questions/31140819/privatefontcollection-with-gdi-sometimes-uses-the-wrong-fontstyle-in-windows-8 I'm currently in touch with a Microsoft Developer Support Engineer. I've also briefly experimented with a Workaround where I use WPF FormattedText to draw the text and then convert the Geometry to GraphicsPath. Seems promising but WPF lacks PrivateFontCollection which we need. Might submit that as an answer if I work out the kinks. – Per Hyyrynen Jun 28 '17 at 14:51

1 Answers1

0

If using WPF is an option you can generate Geometry using FormattedText and then add the Geometry figure by figure to a GraphicsPath object.

to use WPF in a WinForms application you will need a reference to

  • PresentationFramework
  • WindowsBase
  • PresentationCore

these methods might get you started.

    public static readonly int FORMATTED_TEXT_MAX_SIZE = 3579139;

    private static void addStringUsingFormattedText(GraphicsPath targetPath, string text, System.Drawing.FontFamily fontFamily, int fontStyle, float fontSize, System.Drawing.RectangleF layoutRect, System.Drawing.StringFormat stringFormat)
    {
        if (string.IsNullOrEmpty(text))
            return;
        var typeFaceToUse = createTypeFace((System.Drawing.FontStyle)fontStyle, fontFamily.Name);
        var ft = createFormattedText(text, fontSize, layoutRect.Width, layoutRect.Height, typeFaceToUse, stringFormat);
        var geometry = ft.BuildGeometry(new System.Windows.Point(layoutRect.X, layoutRect.Y));
        targetPath.StartFigure();
        foreach (PathFigure pf in geometry.GetFlattenedPathGeometry().Figures)
        {
            foreach (PolyLineSegment polySegment in pf.Segments)
                targetPath.AddPolygon(polySegment.Points.Select(point => new System.Drawing.PointF((float)point.X, (float)point.Y)).ToArray());
        }
        targetPath.CloseFigure();
    }

    private static FormattedText createFormattedText(string text, float fontSize, float width, float height, Typeface typeFaceToUse, System.Drawing.StringFormat stringFormat)
    {
        var ft = new FormattedText(text, System.Globalization.CultureInfo.CurrentCulture, System.Windows.FlowDirection.LeftToRight
            , typeFaceToUse, fontSize, Brushes.Black);
        if (width > 0)
            ft.MaxTextWidth = Math.Min(FORMATTED_TEXT_MAX_SIZE, width);
        if (height > 0)
            ft.MaxTextHeight = Math.Min(FORMATTED_TEXT_MAX_SIZE, height);
        ft.Trimming = TextTrimming.None;

        switch (stringFormat.Alignment)
        {
            case System.Drawing.StringAlignment.Center:
                ft.TextAlignment = TextAlignment.Center;
                break;
            case System.Drawing.StringAlignment.Far:
                ft.TextAlignment = ft.FlowDirection == FlowDirection.LeftToRight ? TextAlignment.Right : TextAlignment.Left;
                break;
            case System.Drawing.StringAlignment.Near:
                ft.TextAlignment = ft.FlowDirection == FlowDirection.LeftToRight ? TextAlignment.Left : TextAlignment.Right;
                break;
        }

        return ft;
    }

We noticed that if we have an alignment set on our StringFormat it behaves slightly differently. You might need to adjust your logic for that.

In our case we were also using Graphics.MeasureString() to anticipate the width of the string. These following methods should work:

    private static System.Drawing.SizeF measureStringUsingFormattedText(string text, System.Drawing.Font measureFont, System.Drawing.RectangleF layoutRect, System.Drawing.StringFormat stringFormat)
    {
        if (string.IsNullOrEmpty(text))
            return new System.Drawing.SizeF(0, 0);
        var typeFaceToUse = createTypeFace(measureFont.Style, measureFont.Name);
        var ft = createFormattedText(text, measureFont.Size, layoutRect.Width, layoutRect.Height, typeFaceToUse, stringFormat);
        bool includeTrailingSpaces = System.Drawing.StringFormatFlags.MeasureTrailingSpaces == (stringFormat.FormatFlags & System.Drawing.StringFormatFlags.MeasureTrailingSpaces);
        var rectangleToReturn = new System.Drawing.SizeF((float)(includeTrailingSpaces ? ft.WidthIncludingTrailingWhitespace : ft.Width), (float)ft.Height);
        return rectangleToReturn;
    }

    private static System.Drawing.SizeF measureStringUsingFormattedText(string text, System.Drawing.Font measureFont, System.Drawing.SizeF layoutArea, System.Drawing.StringFormat stringFormat, out int linesFilled)
    {
        if (string.IsNullOrEmpty(text))
        {
            linesFilled = 0;
            return new System.Drawing.SizeF(0, 0);
        }

        var typeFaceToUse = createTypeFace(measureFont.Style, measureFont.Name);
        var ft = createFormattedText(text, measureFont.Size, layoutArea.Width, layoutArea.Height, typeFaceToUse, stringFormat);
        bool includeTrailingSpaces = System.Drawing.StringFormatFlags.MeasureTrailingSpaces == (stringFormat.FormatFlags & System.Drawing.StringFormatFlags.MeasureTrailingSpaces);
        var rectangleToReturn = new System.Drawing.SizeF((float)(includeTrailingSpaces ? ft.WidthIncludingTrailingWhitespace : ft.Width), (float)ft.Height);
        linesFilled = (int)Math.Floor(ft.Height / ft.Baseline);
        return rectangleToReturn;
    }

For us the complicated part is how to get the TypeFace object. You can not convert a System.Drawing.FontFamily to a System.Windows.Media.TypeFace object. As long as you do not need a PrivateFontCollection this is no problem.

    private static Typeface createTypeFace(System.Drawing.FontStyle fontStyle, string familyName)
    {
        var fwToUse = System.Drawing.FontStyle.Bold == (fontStyle & System.Drawing.FontStyle.Bold) ? FontWeights.Bold : FontWeights.Normal;
        var fsToUse = System.Drawing.FontStyle.Italic == (fontStyle & System.Drawing.FontStyle.Italic) ? FontStyles.Italic : FontStyles.Normal;
        // Create and return a System.Windows.Media.TypeFace object...
        // if you previously used PrivateFontCollection you might need to load the font from a file.
    }

I haven't done proper performance tests on this. I got the feeling this was slightly slower then just using GDI+.

Per Hyyrynen
  • 81
  • 1
  • 14