简体   繁体   English

使用 System.Drawing 在 C# 中的 Bitmap 上的高效文本渲染

[英]Efficient Text Rendering On Bitmap In C# with System.Drawing

I'm writing a program that takes bitmaps and writes some text onto them.我正在编写一个程序,它采用位图并将一些文本写入它们。 I have a functional method that will do this, I provide it with the string that needs to be drawn, the rectangle it needs to be fitted to, the name of the font family to use, gets the largest size it possibly can for the font and then draws it onto the provided bitmap (centred to the rectangle if that is what is desired)我有一个函数方法可以做到这一点,我为它提供了需要绘制的字符串、需要拟合的矩形、要使用的字体系列的名称、获得字体可能的最大尺寸然后将其绘制到提供的 bitmap 上(如果需要,以矩形为中心)

The code I am currently using is as follow我目前使用的代码如下

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;
        }

The problem is that this code looks ugly and its kind of slow as well.问题是这段代码看起来很丑,而且速度也很慢。 So I suppose I was just wondering if there was a better way to do this, some sort of function call or property I had missed somewhere when trying to make this all work, since I've managed to get the rest of the programs bitmap manipulation to a rather clean state, its just this, which looks kinda awful.所以我想我只是想知道是否有更好的方法来做到这一点,某种 function 调用或我在尝试使这一切正常工作时错过的某个地方的属性,因为我已经设法获得程序 rest对于一个相当干净的 state,它就是这个,看起来有点糟糕。

Thankyou in advance for any help given on this matter.预先感谢您在此问题上提供的任何帮助。

So I took the advise of @Raju Joseph in the comments of the question and broke the code up.因此,我在问题的评论中采纳了@Raju Joseph 的建议并分解了代码。 It's probably not any faster to run now than before but at least it looks neater so the function that draws the text now looks like this现在运行起来可能不会比以前快,但至少看起来更整洁,所以绘制文本的 function 现在看起来像这样

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;
        }

With figuring out how big the font needs to be being handled by a different method of the class which is as follow通过确定需要通过 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);
        }

Probably some other way of doing that but this seems fast enough and it looks neat enough in source so thumbs up for that.可能有其他方法可以做到这一点,但这似乎足够快,而且在源代码中看起来足够整洁,所以对此竖起大拇指。

In terms of performance, there's two reasons for your performance issues.在性能方面,您的性能问题有两个原因。

1) Loading fonts is a time consuming process in general, even outside of .NET (Think of how slow Word or any other program is to draw the fonts when you open the font list). 1)加载 fonts 通常是一个耗时的过程,即使在 .NET 之外(想想当你打开字体列表时 Word 或任何其他程序绘制 Z980D14C0C85495B48B9A9134658E6121 有多慢)。 So try to find ways to cache the font objects in your class if you can instead of recreating them each time.因此,如果可以的话,请尝试找到将字体对象缓存在 class 中的方法,而不是每次都重新创建它们。

2) GDI is faster than GDI+ as per the 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 : "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+." 2)根据文档,GDI 比 GDI+ 更快:“您可以选择 GDI 或 GDI+ 进行文本渲染;但是,GDI 通常提供更好的性能和更准确的文本测量。”,因此请尝试使用DrawText ,因为再次按照文档:“使用 TextRenderer class 中的 DrawText 方法,您可以访问 GDI 功能以在表单或控件上绘制文本。GDI 文本呈现通常提供比 GDI+ 更好的性能和更准确的文本测量。”

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM