使用 System.Drawing 在 C# 中的位图上高效文本渲染
Efficient Text Rendering On Bitmap In C# with System.Drawing
我正在编写一个程序,它接受位图并在上面写入一些文本。我有一个 functional 方法可以做到这一点,我为它提供了需要绘制的字符串、需要适合的矩形、要使用的字体系列的名称,获取字体的最大尺寸,然后将其绘制到提供的位图上(如果需要,则以矩形为中心)
我目前使用的代码如下
public static Bitmap Write_Text(Bitmap i, string s, Rectangle r, string font, bool centered, bool bold)
{
//TODO check that there isnt a better way to do this
//first off we need to make sure this rectangle we are given remains in the bounds
//of the bitmap it will be drawn on
//since pixels start at (0,0) we need the combined origin and dimension of the rectangle
//to be of a lesser value than the dimenion of the rectangle (since = could give out of bounds)
if((r.Width + r.X)<i.Width && (r.Height + r.Y) < i.Height && r.X >= 0 && r.Y >= 0)
{
//now we need to ensure that the graphics object that
//draws the text is properly disposed of
using(Graphics g = Graphics.FromImage(i))
{
//The graphics object will have some settings tweaked
//to ensure high quality rendering
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
//Normally Compositing Mode Would Be Set But Writing Text requires its own non enum setting
//and so is excluded here
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
//and one more dedicated to ensuring the text renders with nice contrast
//and non jagged letters
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
//now since we need to actually loop over to try and fit the text to the box
//we need a control variable for a do-while loop
bool fits = false;
//and storage for the parameter for the fonts size
//the font can't actually be any larger than the smaller
//dimension of the box it goes in
int size = Math.Min(r.Width, r.Height);
do
{
//now a font family may not exist on the computer microsofts
//sans seriff will be used so no need for try catches
Font f;
//If the font is to be bold set it as such
if (bold)
{
f = new Font(font, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else
{
f = new Font(font, size, GraphicsUnit.Pixel);
}
//now we measure the string and if it fits inside the rectangle we can proceed
if(g.MeasureString(s,f).Width <= r.Width && g.MeasureString(s,f).Height <= r.Height)
{
fits = true;
}
else
{
//if the string doesnt fit the box decrease the size and try again
size--;
}
//regardless dispose of f to avoid memory leaks
f.Dispose();
}
while (!fits);
//now we just need to make a string attributes object since the string may want to be centered
StringFormat Format = new StringFormat();
if (centered)
{
Format.Alignment = StringAlignment.Center;
Format.LineAlignment = StringAlignment.Center;
}
//now construct the font object that will be used for the drawing
//as above
Font ff;
if (bold)
{
ff = new Font(font, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else
{
ff = new Font(font, size, GraphicsUnit.Pixel);
}
//now draw the text in place on the bitmap
g.DrawString(s, ff, Brushes.Black, r, Format);
//dispose of the font so its not leaking memory
ff.Dispose();
Format.Dispose();
}
}
return i;
}
问题是这段代码 看起来 丑陋而且有点慢。所以我想我只是想知道是否有更好的方法来做到这一点,某种函数调用或 属性 我在尝试使这一切正常工作时错过了某个地方,因为我已经设法获得了其余的程序位图操作到一个相当干净的状态,就是这个,看起来有点糟糕。
提前感谢您在此问题上提供的任何帮助。
所以我在问题的评论中采纳了@Raju Joseph 的建议并分解了代码。现在 运行 可能没有比以前快多少,但至少看起来更整洁了
所以绘制文本的函数现在看起来像这样
public static Bitmap Write_Text(Bitmap i, string s, Rectangle r, bool centered, string font, bool bold, bool italic)
{
//Since we want to avoid out of bounds errors make sure the rectangle remains within the bounds of the bitmap
//and only execute if it does
if(r.X>= 0 && r.Y>=0&&(r.X+r.Width < i.Width) && (r.Y + r.Height < i.Height))
{
//Step one is to make a graphics object that will draw the text in place
using (Graphics g = Graphics.FromImage(i))
{
//Set some of the graphics properties so that the text renders nicely
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
//Compositing Mode can't be set since string needs source over to be valid
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
//And an additional step to make sure text is proper anti-aliased and takes advantage
//of clear type as necessary
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
//this also requires a font object we need to make sure we dispose of properly
using (Font f = Functions.Generate_Font(s, font, r, bold, italic))
{
//the using function actually makes sure the font is as large as it can be for the
//purpose of fitting the rectangle we just need to check if its centered
using (StringFormat format = new StringFormat())
{
//the format won't always be doing anything but
//just in case we need it
//and if the text is centered we need to tell the formatting
if (centered)
{
format.Alignment = StringAlignment.Center;
format.Alignment = StringAlignment.Center;
}
//and draw the text into place
g.DrawString(s, f, Brushes.Black, r, format);
}
}
}
}
return i;
}
通过 class 的不同方法计算出需要处理多大的字体,如下
public static Font Generate_Font(string s,string font_family, Rectangle r, bool bold, bool italic)
{
//First things first, the font can't be of a size larger than the rectangle in pixels so
//we need to find the smaller dimension as that will constrain the max size
int Max_Size = Math.Min(r.Width, r.Height);
//Now we loop backwards from this max size until we find a size of font that fits inside the
//rectangle given
for(int size = Max_Size; size > 0; size--)
{
//Since a default font is used if the font family specified doesnt exist
//checking the family exists isnt necessary
//However we need to cover if the font is bold or italic
Font f;
if (bold)
{
f = new Font(font_family, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else if (italic)
{
f = new Font(font_family, size, System.Drawing.FontStyle.Italic, GraphicsUnit.Pixel);
}
else if (bold && italic)
{
//the pipe is a bitwise or and plays with the enum flags to get both bold and italic
f = new Font(font_family, size, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Italic, GraphicsUnit.Pixel);
}
else
{
//otherwise make a simple font
f = new Font(font_family, size, GraphicsUnit.Pixel);
}
//because graphics are weird we need a bitmap and graphics object to measure the string
//we also need a sizef to store the measured results
SizeF result;
using(Bitmap b = new Bitmap(100,100))
{
using(Graphics g = Graphics.FromImage(b))
{
result = g.MeasureString(s, f);
}
}
//if the new string fits the constraints of the rectangle we return it
if(result.Width<= r.Width && result.Height <= r.Height)
{
return f;
}
//if it didnt we dispose of f and try again
f.Dispose();
}
//If something goes horribly wrong and no font size fits just return comic sans in 12 pt font
//that won't upset anyone and the rectangle it will be drawn to will clip the excess anyway
return new Font("Comic Sans", 12, GraphicsUnit.Point);
}
可能还有其他方法可以做到这一点,但这看起来足够快,而且在源代码中看起来足够整洁,所以赞成。
在性能方面,您的性能问题有两个原因。
1) 加载字体通常是一个耗时的过程,即使在 .NET 之外也是如此(想想当您打开字体列表时 Word 或任何其他程序绘制字体的速度有多慢)。因此,如果可以,请尝试找到将字体对象缓存在 class 中的方法,而不是每次都重新创建它们。
2) 根据 docs: "You can choose either GDI or GDI+ for text rendering; however, GDI generally offers better performance and more accurate text measuring.", so try to use DrawText
instead because, again as per the docs,GDI 比 GDI+ 快:"With the DrawText method in the TextRenderer class, you can access GDI functionality for drawing text on a form or control. GDI text rendering typically offers better performance and more accurate text measuring than GDI+."
我正在编写一个程序,它接受位图并在上面写入一些文本。我有一个 functional 方法可以做到这一点,我为它提供了需要绘制的字符串、需要适合的矩形、要使用的字体系列的名称,获取字体的最大尺寸,然后将其绘制到提供的位图上(如果需要,则以矩形为中心)
我目前使用的代码如下
public static Bitmap Write_Text(Bitmap i, string s, Rectangle r, string font, bool centered, bool bold)
{
//TODO check that there isnt a better way to do this
//first off we need to make sure this rectangle we are given remains in the bounds
//of the bitmap it will be drawn on
//since pixels start at (0,0) we need the combined origin and dimension of the rectangle
//to be of a lesser value than the dimenion of the rectangle (since = could give out of bounds)
if((r.Width + r.X)<i.Width && (r.Height + r.Y) < i.Height && r.X >= 0 && r.Y >= 0)
{
//now we need to ensure that the graphics object that
//draws the text is properly disposed of
using(Graphics g = Graphics.FromImage(i))
{
//The graphics object will have some settings tweaked
//to ensure high quality rendering
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
//Normally Compositing Mode Would Be Set But Writing Text requires its own non enum setting
//and so is excluded here
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
//and one more dedicated to ensuring the text renders with nice contrast
//and non jagged letters
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
//now since we need to actually loop over to try and fit the text to the box
//we need a control variable for a do-while loop
bool fits = false;
//and storage for the parameter for the fonts size
//the font can't actually be any larger than the smaller
//dimension of the box it goes in
int size = Math.Min(r.Width, r.Height);
do
{
//now a font family may not exist on the computer microsofts
//sans seriff will be used so no need for try catches
Font f;
//If the font is to be bold set it as such
if (bold)
{
f = new Font(font, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else
{
f = new Font(font, size, GraphicsUnit.Pixel);
}
//now we measure the string and if it fits inside the rectangle we can proceed
if(g.MeasureString(s,f).Width <= r.Width && g.MeasureString(s,f).Height <= r.Height)
{
fits = true;
}
else
{
//if the string doesnt fit the box decrease the size and try again
size--;
}
//regardless dispose of f to avoid memory leaks
f.Dispose();
}
while (!fits);
//now we just need to make a string attributes object since the string may want to be centered
StringFormat Format = new StringFormat();
if (centered)
{
Format.Alignment = StringAlignment.Center;
Format.LineAlignment = StringAlignment.Center;
}
//now construct the font object that will be used for the drawing
//as above
Font ff;
if (bold)
{
ff = new Font(font, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else
{
ff = new Font(font, size, GraphicsUnit.Pixel);
}
//now draw the text in place on the bitmap
g.DrawString(s, ff, Brushes.Black, r, Format);
//dispose of the font so its not leaking memory
ff.Dispose();
Format.Dispose();
}
}
return i;
}
问题是这段代码 看起来 丑陋而且有点慢。所以我想我只是想知道是否有更好的方法来做到这一点,某种函数调用或 属性 我在尝试使这一切正常工作时错过了某个地方,因为我已经设法获得了其余的程序位图操作到一个相当干净的状态,就是这个,看起来有点糟糕。
提前感谢您在此问题上提供的任何帮助。
所以我在问题的评论中采纳了@Raju Joseph 的建议并分解了代码。现在 运行 可能没有比以前快多少,但至少看起来更整洁了 所以绘制文本的函数现在看起来像这样
public static Bitmap Write_Text(Bitmap i, string s, Rectangle r, bool centered, string font, bool bold, bool italic)
{
//Since we want to avoid out of bounds errors make sure the rectangle remains within the bounds of the bitmap
//and only execute if it does
if(r.X>= 0 && r.Y>=0&&(r.X+r.Width < i.Width) && (r.Y + r.Height < i.Height))
{
//Step one is to make a graphics object that will draw the text in place
using (Graphics g = Graphics.FromImage(i))
{
//Set some of the graphics properties so that the text renders nicely
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
//Compositing Mode can't be set since string needs source over to be valid
g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality;
//And an additional step to make sure text is proper anti-aliased and takes advantage
//of clear type as necessary
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
//this also requires a font object we need to make sure we dispose of properly
using (Font f = Functions.Generate_Font(s, font, r, bold, italic))
{
//the using function actually makes sure the font is as large as it can be for the
//purpose of fitting the rectangle we just need to check if its centered
using (StringFormat format = new StringFormat())
{
//the format won't always be doing anything but
//just in case we need it
//and if the text is centered we need to tell the formatting
if (centered)
{
format.Alignment = StringAlignment.Center;
format.Alignment = StringAlignment.Center;
}
//and draw the text into place
g.DrawString(s, f, Brushes.Black, r, format);
}
}
}
}
return i;
}
通过 class 的不同方法计算出需要处理多大的字体,如下
public static Font Generate_Font(string s,string font_family, Rectangle r, bool bold, bool italic)
{
//First things first, the font can't be of a size larger than the rectangle in pixels so
//we need to find the smaller dimension as that will constrain the max size
int Max_Size = Math.Min(r.Width, r.Height);
//Now we loop backwards from this max size until we find a size of font that fits inside the
//rectangle given
for(int size = Max_Size; size > 0; size--)
{
//Since a default font is used if the font family specified doesnt exist
//checking the family exists isnt necessary
//However we need to cover if the font is bold or italic
Font f;
if (bold)
{
f = new Font(font_family, size, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
}
else if (italic)
{
f = new Font(font_family, size, System.Drawing.FontStyle.Italic, GraphicsUnit.Pixel);
}
else if (bold && italic)
{
//the pipe is a bitwise or and plays with the enum flags to get both bold and italic
f = new Font(font_family, size, System.Drawing.FontStyle.Bold | System.Drawing.FontStyle.Italic, GraphicsUnit.Pixel);
}
else
{
//otherwise make a simple font
f = new Font(font_family, size, GraphicsUnit.Pixel);
}
//because graphics are weird we need a bitmap and graphics object to measure the string
//we also need a sizef to store the measured results
SizeF result;
using(Bitmap b = new Bitmap(100,100))
{
using(Graphics g = Graphics.FromImage(b))
{
result = g.MeasureString(s, f);
}
}
//if the new string fits the constraints of the rectangle we return it
if(result.Width<= r.Width && result.Height <= r.Height)
{
return f;
}
//if it didnt we dispose of f and try again
f.Dispose();
}
//If something goes horribly wrong and no font size fits just return comic sans in 12 pt font
//that won't upset anyone and the rectangle it will be drawn to will clip the excess anyway
return new Font("Comic Sans", 12, GraphicsUnit.Point);
}
可能还有其他方法可以做到这一点,但这看起来足够快,而且在源代码中看起来足够整洁,所以赞成。
在性能方面,您的性能问题有两个原因。
1) 加载字体通常是一个耗时的过程,即使在 .NET 之外也是如此(想想当您打开字体列表时 Word 或任何其他程序绘制字体的速度有多慢)。因此,如果可以,请尝试找到将字体对象缓存在 class 中的方法,而不是每次都重新创建它们。
2) 根据 docs: "You can choose either GDI or GDI+ for text rendering; however, GDI generally offers better performance and more accurate text measuring.", so try to use DrawText
instead because, again as per the docs,GDI 比 GDI+ 快:"With the DrawText method in the TextRenderer class, you can access GDI functionality for drawing text on a form or control. GDI text rendering typically offers better performance and more accurate text measuring than GDI+."