I'm working with drawing a long string to a bitmap (talking more than a million characters), including multiline characters \r\n
, written by a StringBuilder
.
My Text to Bitmap code is as follows:
public static Bitmap GetBitmap(string input, Font inputFont,
Color bmpForeground, Color bmpBackground) {
Image bmpText = new Bitmap(1, 1);
try {
// Create a graphics object from the image.
Graphics g = Graphics.FromImage(bmpText);
// Measure the size of the text when applied to image.
SizeF inputSize = g.MeasureString(input, inputFont);
// Create a new bitmap with the size of the text.
bmpText = new Bitmap((int)inputSize.Width,
(int)inputSize.Height);
// Instantiate graphics object, again, since our bitmap
// was modified.
g = Graphics.FromImage(bmpText);
// Draw a background to the image.
g.FillRectangle(new Pen(bmpBackground).Brush,
new Rectangle(0, 0,
Convert.ToInt32(inputSize.Width),
Convert.ToInt32(inputSize.Height)));
// Draw the text to the image.
g.DrawString(input, inputFont,
new Pen(bmpForeground).Brush, new PointF(0, 0));
} catch {
// Draw a blank image with background.
Graphics.FromImage(bmpText).FillRectangle(
new Pen(bmpBackground).Brush,
new Rectangle(0, 0, 1, 1));
}
return (Bitmap)bmpText;
}
Normally, it works as expected -- but only when used for single characters. However, the issue lies when a multitude of characters are used. Simply put, extra lines appear both vertically and horizontally when drawn on to the image.
This effect is demonstrated here, zoomed in 1:1 (see the full Image):
However, I can render this same text in Notepad++ with just the output of the string and it's essentially as expected:
I can view it in any other text viewer; the result will be the same.
So how, and why, is the program rendering the Bitmap with those 'extra' lines?
When drawing a string of characters (ASCII or a form of Unicode encoded symbols) using Graphics.DrawString() with a fixed sized Font, the resulting graphics appear to generate a sort of grid, degrading the visual quality of the rendering.
A solution is to substitute the GDI+ Graphics methods with the GDI methods, using TextRenderer.MeasureText() and TextRenderer.DrawText().
Sample code to correct the problem and to reproduce it.
Load a text file using the default local CodePage encoding. The source text has been saved without any Unicode encoding. If a different encoding is used, the Encoding.Default
must be substituted with the actual Encoding (e.g. Encoding.Unicode
, Encoding.UTF8
...).
The fixed size Font used for all tests is Lucida Console, 4em Regular
.
Other candidates, usually available, are Consolas
and Courier New
.
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using System.Text;
using System.Windows.Forms;
// Read the input text - assume UTF8 Encoding
string text = File.ReadAllText([Source text Path], Encoding.UTF8);
Font font = new Font("Lucida Console", 4, FontStyle.Regular, GraphicsUnit.Point);
// Use TextRenderer
using (var bitmap = ASCIIArtBitmap(text, font))
bitmap.Save(@"[FilePath1]", ImageFormat.Png);
// Use GDI+ Graphics
using (var bitmap = ASCIIArtBitmapGdiPlus(text, font))
bitmap.Save(@"[FilePath2]", ImageFormat.Png);
// Use GraphicsPath
using (var bitmap = ASCIIArtBitmapGdiPlusPath(text, font))
bitmap.Save(@"[FilePath3]", ImageFormat.Png);
font.Dispose();
TextRenderer
is first used to eliminate the visual defect reported.
As a note, both TextRederer.MeasureText()
and Graphics.DrawString()
, per the MSDN documentation, should be used to measure a single line of text.
Anyway, it's also known that the text is correctly measured when the text is composed of multiple lines, if a line feed separates those lines.
It can be easily tested, splitting the source text with Environment.Newline
as separator and multiplying the number of lines by the height of a single line. The result is always the same.
private Bitmap ASCIIArtBitmap(string text, Font font)
{
var flags = TextFormatFlags.Top | TextFormatFlags.Left |
TextFormatFlags.NoPadding | TextFormatFlags.NoClipping;
Size bitmapSize = TextRenderer.MeasureText(text, font, Size.Empty, flags);
var bitmap = new Bitmap(bitmapSize.Width, bitmapSize.Height, PixelFormat.Format24bppRgb)
using (var g = Graphics.FromImage(bitmap)) {
bitmapSize = TextRenderer.MeasureText(g, text, font, new Size(bitmap.Width, bitmap.Height), flags);
TextRenderer.DrawText(g, text, font, Point.Empty, Color.Black, Color.White, flags);
return bitmap;
}
}
Low resolution rendering, (150 x 55 chars). No grid effect visible.
Using Graphics.DrawString()
, to reproduce the reported behavior.
TextRenderingHint.AntiAlias is specified, as an effort to reduce the visual defect. CompositingQuality.HighSpeed seems out of place, but it actually, in this case, renders better than HighQuality
.
TextContrast = 1 makes the resulting image a little darker. With the default setting is too bright and loses details (my opinion, though).
private Bitmap ASCIIArtBitmapGdiPlus(string text, Font font)
{
using (var modelbitmap = new Bitmap(10, 10, PixelFormat.Format24bppRgb))
using (var modelgraphics = Graphics.FromImage(modelbitmap))
{
modelgraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
SizeF bitmapSize = modelgraphics.MeasureString(text, font, Point.Empty, StringFormat.GenericTypographic);
var bitmap = new Bitmap((int)bitmapSize.Width, (int)bitmapSize.Height, PixelFormat.Format24bppRgb);
using (var g = Graphics.FromImage(bitmap))
{
g.Clear(Color.White);
g.TextRenderingHint = TextRenderingHint.AntiAlias;
g.CompositingQuality = CompositingQuality.HighSpeed;
g.TextContrast = 1;
g.DrawString(text, font, Brushes.Black, PointF.Empty, StringFormat.GenericTypographic);
return bitmap;
}
}
}
Medium-low resolution (300 x 110 chars), the grid-effect is visible.
Graphics.DrawString() TextRenderer.DrawText()
Another approach, using GraphicsPath.AddString()
The resulting bitmap is a somewhat better, but the grid effect is there anyway.
What can be really noticed, is the difference in speed. GraphicsPath is way slower than all other methods tested.
private Bitmap ASCIIArtBitmapGdiPlusPath(string text, Font font)
{
using (var path = new GraphicsPath(FillMode.Alternate)) {
path.AddString(text, font.FontFamily, (int)font.Style, 4, Point.Empty, StringFormat.GenericTypographic);
var gpRect = Rectangle.Round(path.GetBounds());
var bitmap = new Bitmap(gpRect.Width, gpRect.Height);
using (var g = Graphics.FromImage(bitmap)) {
g.Clear(Color.White);
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit; // Not used
g.PixelOffsetMode = PixelOffsetMode.Half; // <= for the Bitmap
g.SmoothingMode = SmoothingMode.AntiAlias; // <= for the GraphicsPath
g.FillPath(Brushes.Black, path);
return bitmap;
}
}
}
Why the rendering quality is, in this case, so different?
All depends on the nature of the GDI+ resolution-independent grid-fitted rendering.
From an obscure document, of Microsoft's origin, found on the WayBack Machine:
GDI+ Text, Resolution Independence, and Rendering Methods.
Grid Fitting, also known as hinting, is the process of adjusting the position of pixels in a rendered glyph to make the glyph easily legible at smaller sizes. Techniques include aligning glyph stems on whole pixels and ensuring similar features of a glyph are affected equally.
In an effort to compensates for Grid Fitting, trying to achieve the best appearance possible for a text, the typographic tracking (usually called letter-spacing), is modified.
When GDI+ displays a line of grid fitted glyphs that are shorter than their design width, it follows these general rules:
- The line is allowed to contract by up to an
em
without any change of glyph spacing.- Remaining contraction is made up by increasing the width of any spaces between words, to a maximum of doubling.
- Remaining contraction is made up by introducing blanks pixels between glyphs.
This "effort" seems to be pushed to the point of modifying the kerning pairs of the glyphs.
In a proportional Font, the visual rendering has a benefit, but with a fixed size Font, the previously mentioned calculation produces a sort of grid-alignment, clearly visible when the same symbol is repeated multiple times.
TextRenderer GDI methods, based on clear-type rendering - aimed to the visual rendering of text on screen - uses a sub-pixel representation of glyphs. The calculation of letter-spacing is completely different.
Microsoft ClearType overview.
ClearType works by accessing the individual vertical color stripe elements in every pixel of an LCD screen. Before ClearType, the smallest level of detail that a computer could display was a single pixel, but with ClearType running on an LCD monitor, we can now display features of text as small as a fraction of a pixel in width.
The extra resolution increases the sharpness of the tiny details in text display, making it much easier to read over long durations.
The drawback is that this method of calculating the letter-spacing, makes it unsuited for printing from WinForms. MSDN documentation repeatedly states this.
Other interesting resources on the subject:
The Art of dev - Text rendering methods comparison or GDI vs. GDI+
Why does my text look different in GDI+ and in GDI?
GDI vs. GDI+ Text Rendering Performance
StackOverflow answers:
Why is Graphics.MeasureString() returning a higher than expected number?
Modifying the kerning in System.Drawing.Graphics.DrawString()
Application used to generate the ASCII-art text:
ASCII Generator 2 on SourceForge (free software)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With