4

We need to:

  1. Measure text accurately.
  2. Render text line by line to a screen graphics context in the presence of translation and scaling transforms applied to the graphics context.
  3. Hit testing: allow text to be selected precisely with the mouse or via a displayed caret.
  4. Print the result if needed, and as accurately as possible, with a printer. Note: this is secondary. Screen rendering and hit testing are primary.
  5. Run on Windows XP and higher operating systems.

within a WinForms application that is also rendering graphics and images to the same graphics context.

There are four technologies that we've encountered. We've tried using the first two, and ran into the issues described, over the course of several months.

GDI+

Purportedly resolution-independent text. However according to this question - and other sources - this technology is to be avoided because of quality issues.

MSDN states that calling Graphics.MeasureString along with StringFormat.GenericTypographic and TextRenderingHint.AntiAlias produces accurate string measurement. However, in our experience, and that of others, this is not the case - we do not get accurate string measurements.

  • Pros: Fast
  • Cons: inaccurate string measurement.

Result: unusable because of inaccurate string measurement.

GDI via TextRenderer

This was introduced to overcome the limitations of GDI+. However this introduced limitations of its own:

Result: unusuable for these reasons

GDI via p/invoke

Calling GetTextExtentExPoint for text measurement and DrawText / DrawTextEx / ExtTextOut for rendering.

We haven't tried this yet.

DirectWrite

This seems promising, since it interoperates with other technologies including GDI/GDI+, so presumably the rest of our graphics rendering wouldn't change. However it is only available for Windows Vista and more recent Windows versions. This is presently a problem since Windows XP still has a significant installed base.

Question

Which of these technologies can be made to work given the requirements?

Note: There's much misinformation about this topic floating around, so please answer this question only if you have expertise in this area. Also, please don't suggest WPF - that isn't something we're considering using.

Community
  • 1
  • 1
bright
  • 4,700
  • 1
  • 34
  • 59
  • 1
    What about DirectWrite? Could you also provide some more information about what inaccuracies GDI+ has? What is the percentage error? What precision do you need? – Ani May 24 '12 at 13:22
  • Good point, will add a section on our experience with DirectWrite - basically the platform requirements (Vista+) seemed too restrictive. XP still dominates. – bright May 24 '12 at 17:14
  • Can you provide more information on how you determined the GDI+ measurements were not accurate? Also, can you clarify if you're doing this just for a printer, or if you want to draw on screen and then reproduce the result on a printer. – Adrian McCarthy May 24 '12 at 17:34
  • @Adrian, clarified that we do need screen rendering. Will add information about the GDI+ measurements. – bright May 24 '12 at 17:45
  • Added the hit testing requirement explicitly - although the accuracy requirement pretty much covers it. Also added that printing is secondary. – bright May 25 '12 at 00:29
  • [GDI certainly supports transforms](http://msdn.microsoft.com/en-us/library/dd183475). [For text, see this post](http://www.ucancode.net/CPP_Library_Control_Tool/SetWorldTransform_Draw_Rotate_Slant_Text_String_VC_Article.htm). – Ben Voigt May 25 '12 at 00:35
  • @Ben, I haven't said that GDI per se doesn't. GDI via TextRenderer does not work well with transforms, according to the linked post. We're talking about bugs in the implementation. – bright May 25 '12 at 01:31

3 Answers3

4

Supposedly the MeasureCharacterRanges function is more accurate than MeasureString.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 3
    What finally worked was using MeasureCharacterRanges with StringFormat = StringFormat.GenericTypographic together with FormatFlags.MeasureTrailingSpaces | FormatFlags.NoWrap | FormatFlags.NoClip. Thanks. – bright Jul 15 '12 at 16:10
3

If you have to target both the screen and a printer, then you need to make some decisions about your approach before deciding which rendering engine to use. Here are some possibilities:

  1. Lay out in screen units and do a best-possible approximation for the printer.
  2. Lay out in printer units and do a best-possible approximation for the screen.
  3. Lay out in a theoretical high-resolution space and do a best-possible approximation for both printer and screen.

In all cases, the best-possible approximation might be done by very careful approximations at each step, which can give the highest fidelity for each device at the cost of accuracy in matching, or you could render to bitmap and scale to the device, which gives lower fidelity but a the best-possible approximation to the other space.

I've done hard-core print preview with GDI. It's hard, but do-able for regular layouts. But if you're handling arbitrary transforms, especially rotations other than +/-90 degrees, it's nigh impossible.

It's very easy to make subtle mistakes and believe that the measurements you get back are wrong.

  • Font hinting makes font scaling non-linear when the stroke widths are on the same order of magnitude as the dpi, so if you get a width w for some text, then the width if you double the size of the font may not be exactly 2*w.

  • Font substitution is common in many printer drivers. Even if you choose a TrueType or OpenType font you might not get the same font on the printer as you got on the screen.

  • Non-square pixels are common in some printers. These can mess up even decent rasterizers when you're doing something that's not axis-aligned.

  • Kerning can be surprising. Don't assume that width('a') + width('b') == width("ab").

  • Rounding errors are easy to make. Sometimes you have to know the rounding rules used by the underlying rasterizer.

  • Measuring in the wrong context is too common. Don't try to measure in a screen context and apply a transform to get to printer units. If you need printer units, measure in the printer context.

I'm of the opinion today that if you need just print preview, then you should lay out in printer units to a bitmap that's sized to the page, and scale the bitmap to the screen based on the ratios of the DPIs. That's the easiest thing to do and it gives good results. But it's not clear if a good hardcopy and print-preview is really what you're after.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
  • Thanks Adrian. To be sure, our primary focus is finding the right API for screen rendering. Printing is also needed, but right now we can't get screen rendering to work precisely enough for hit testing and line width calculation. When you say GDI, do you mean native GDI (via p/invoke) or just the capabilities exposed by TextRenderer? Please be specific about the APIs. Our question was not about general typesetting, but about working around the various issues encountered in these technologies. – bright May 25 '12 at 00:31
  • @bright: I've only ever done detailed stuff with Native GDI. My experience with GDI+ is limited. I'm not a C# person, so I don't know anything about the .NET wrappers for these. I've also done a bit with Uniscribe to get positioning accuracy down to the grapheme cluster level. – Adrian McCarthy May 25 '12 at 12:40
3

I had similar requirements and ran into the same problems with text rendering and scaling. Even my attempts with WPF (.NET 3.5) were a nightmare, for many reasons.

I ended up using GDI+ graphics.DrawString, despite being 'deprecated', with a funny trick to get accurate text measurements.

static public RectangleF MeasureInkBox(Graphics graphics, string text, Font font)
{
    var bounds = new RectangleF();
    using (var textPath = new GraphicsPath())
    {
        textPath.AddString(
            text,
            font.FontFamily,
            (int)font.Style,
            font.Size,
            new PointF(0, 0),
            StringFormat.GenericTypographic );
        bounds = textPath.GetBounds();
    }
    return bounds;
}

The speed and quality turned out to be satisfying. Also, graphics.DrawString is the recommended way to print text with winforms.

Measuring individual character positions can be tricky due to kerning, but I guess a slightly more complex version of this method could do the job. For example, for "Hello" it would measure "H", then "He", then Hel" and so on. It should not degrade performances significantly, especially if you do it on he fly when receiving a click on the word.

Jem
  • 2,255
  • 18
  • 25
  • It might be worth using a binary search with the complex version, if nothing else because it's fairly easy to avoid running as many brute force cases. – Skrylar May 07 '19 at 08:11