Drawing text to a Bitmap with TextRenderer
Asked Answered
G

4

7

I am trying to draw some text using TextRenderer (since this is favorable to using Graphics.DrawString) to a Bitmap, however it is having some very undesirable effects.

Example Code

using (Bitmap buffer = new Bitmap(this.ClientRectangle.Width, this.ClientRectangle.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb))
{
    using (Graphics graphics = Graphics.FromImage(buffer))
    {
        // Produces the result below
        graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
        // Produces clean text, but I'd really like ClearType!
        graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
        TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black);
    }
    e.Graphics.DrawImageUnscaled(buffer, this.ClientRectangle);
}

Result

enter image description here

I'm not sure exactly how to fix this...help!

I do NOT want to use Graphics.DrawString as I want correct GDI (as opposed to GDI+) rendering.

NOTE: I've just realized, I've left a gaping hole in this question. Some people have pointed out that rendering ClearType text is working fine on their machines...

I'm trying to render the text to a transparent (Color.Transparent) bitmap. If I do it with a solid color, everything works fine! (but it is imperative that I render to a transparent Bitmap).

Generalization answered 16/9, 2013 at 22:1 Comment(2)
Try to set the proper Graphics.InterpolationMode and Graphics.SmoothingMode.Calvinna
@HamletHakobyan, I've tried that at least, but neither solve the problem. These seem to work when using Graphics.DrawString, however since TextRenderer is a GDI text drawing system, these properties do not seem to work.Generalization
I
4

Specify a BackColor in your call to DrawText():

TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black, this.BackColor);
Inwards answered 16/9, 2013 at 23:28 Comment(1)
That works, but the background for the text is actually an image, therefore, the BackColor for the text should be transparent. Unfortunately since GDI isn't alpha aware, using Color.Transparent doesn't blend ClearType text properly.Generalization
A
4

You can try setting TextRenderingHint for your Image Graphics:

using (Graphics graphics = Graphics.FromImage(buffer))
{
    graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
    TextRenderer.DrawText(graphics, "Hello World", this.Font, this.ClientRectangle, Color.Black);
}
Amaris answered 17/9, 2013 at 2:36 Comment(7)
Works OK for AntiAliased text, but not for ClearType. It is the latter that I would like.Generalization
@series0ne why do you need ClearType here? What do you mean by not for ClearType? you should update the code to show your try.Amaris
By default (on my systems at least), the TextRenderingHint is ClearType. Therefore, my code omits the TextRenderingHint, since the default is already ClearType, therefore there is no need to explicitly define the TextRenderingHint.Generalization
I think this might solve the issue...#2692043Generalization
@series0ne it's just you don't want to use TextRenderingHint, I don't know why your Graphics.TextRenderingHint has default value of ClearType, I tested to print the default value and it's SystemDefault, TextRenderingHint = TextRenderingHint.ClearTypeGridFit also works.Amaris
I really couldn't care less for using TextRenderingHint, or not. Either way, it IS being used in the background regardless of whether I set it explicitly. - See my updated code example.Generalization
To answer this comment: "I don't know why your Graphics.TextRenderingHint has default value of ClearType, I tested to print the default value and it's SystemDefault" - Yes It's SystemDefault on my machine too. My point was that the default text rendering mechanism on my machine is ClearType, therefore, On my machine, SystemDefault and ClearTypeGridFit are the same. - On an older machine (say Win2k), the SystemDefault would likely be AntiAlias or AntiAliasGridFit. Hope that makes sense.Generalization
V
4

The issue is that TextRenderer uses GDI rendering that uses ClearType to render text, clear-type uses special anti-aliasing algorithm to smooth the text, unfortunately it doesn't work when you try to draw on bitmap device.

To make it work you have to use a trick, draw into in-memory and then copy to bitmap:

  1. Create in-memory bitmap buffer that is compatible with display device context (IntPtr.Zero handle)
  2. Fill the buffer background with solid color or image
  3. Render the text into the memory bitmap
  4. Copy from in-memory bitmap to image device context (BitBlt)

See this blog for details: GDI text rendering to image.

Sample code, sorry its a bit long:

public static class Test
{
    public static Image Render()
    {
        // create the final image to render into
        var image = new Bitmap(190, 30, PixelFormat.Format32bppArgb);

        // create memory buffer from desktop handle that supports alpha channel
        IntPtr dib;
        var memoryHdc = CreateMemoryHdc(IntPtr.Zero, image.Width, image.Height, out dib);
        try
        {
            // create memory buffer graphics to use for HTML rendering
            using (var memoryGraphics = Graphics.FromHdc(memoryHdc))
            {
                // must not be transparent background 
                memoryGraphics.Clear(Color.White);

                // execute GDI text rendering
                TextRenderer.DrawText(memoryGraphics, "Test string 1", new Font("Arial", 12), new Point(5, 5), Color.Red, Color.Wheat);
                TextRenderer.DrawText(memoryGraphics, "Test string 2", new Font("Arial", 12), new Point(100, 5), Color.Red);
            }

            // copy from memory buffer to image
            using (var imageGraphics = Graphics.FromImage(image))
            {
                var imgHdc = imageGraphics.GetHdc();
                BitBlt(imgHdc, 0, 0, image.Width, image.Height, memoryHdc, 0, 0, 0x00CC0020);
                imageGraphics.ReleaseHdc(imgHdc);
            }
        }
        finally
        {
            // release memory buffer
            DeleteObject(dib);
            DeleteDC(memoryHdc);
        }

        return image;
    }

    private static IntPtr CreateMemoryHdc(IntPtr hdc, int width, int height, out IntPtr dib)
    {
        // Create a memory DC so we can work off-screen
        IntPtr memoryHdc = CreateCompatibleDC(hdc);
        SetBkMode(memoryHdc, 1);

        // Create a device-independent bitmap and select it into our DC
        var info = new BitMapInfo();
        info.biSize = Marshal.SizeOf(info);
        info.biWidth = width;
        info.biHeight = -height;
        info.biPlanes = 1;
        info.biBitCount = 32;
        info.biCompression = 0; // BI_RGB
        IntPtr ppvBits;
        dib = CreateDIBSection(hdc, ref info, 0, out ppvBits, IntPtr.Zero, 0);
        SelectObject(memoryHdc, dib);

        return memoryHdc;
    }

    [DllImport("gdi32.dll")]
    public static extern int SetBkMode(IntPtr hdc, int mode);

    [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
    private static extern IntPtr CreateCompatibleDC(IntPtr hdc);

    [DllImport("gdi32.dll")]
    private static extern IntPtr CreateDIBSection(IntPtr hdc, [In] ref BitMapInfo pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset);

    [DllImport("gdi32.dll")]
    public static extern int SelectObject(IntPtr hdc, IntPtr hgdiObj);

    [DllImport("gdi32.dll")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool BitBlt(IntPtr hdc, int nXDest, int nYDest, int nWidth, int nHeight, IntPtr hdcSrc, int nXSrc, int nYSrc, int dwRop);

    [DllImport("gdi32.dll")]
    public static extern bool DeleteObject(IntPtr hObject);

    [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
    public static extern bool DeleteDC(IntPtr hdc);

    [StructLayout(LayoutKind.Sequential)]
    internal struct BitMapInfo
    {
        public int biSize;
        public int biWidth;
        public int biHeight;
        public short biPlanes;
        public short biBitCount;
        public int biCompression;
        public int biSizeImage;
        public int biXPelsPerMeter;
        public int biYPelsPerMeter;
        public int biClrUsed;
        public int biClrImportant;
        public byte bmiColors_rgbBlue;
        public byte bmiColors_rgbGreen;
        public byte bmiColors_rgbRed;
        public byte bmiColors_rgbReserved;
    }
}
Vestibule answered 29/5, 2014 at 11:52 Comment(2)
Hi, I'm getting an exception from the BitBlt() call. I tried to add CallingConvention = CallingConvention.Cdecl to the import, but it made no difference. Message: A call to PInvoke function 'BitmapRecognition!BitmapRecognition.Text::BitBlt' has unbalanced the stack. This is likely because the managed PInvoke signature does not match the unmanaged target signature. Check that the calling convention and parameters of the PInvoke signature match the target unmanaged signature.Weil
whoops, BitBlt has incorrect signature, change the last argument (dwRop) from long to int. sorry.Vestibule
S
1

I know this question have been asked almost a decade ago but obviously no answer have solved the OPs problem. I just found the solution to this myself and, I guess, this could be a great help to everyone stuck in this problem.

Here's how: the trick is to use BufferedGraphics. Through BufferedGraphics you can draw strings using TextRenderer with full transparency. BufferedGraphics creates a DeviceIndependentBitmap (DIB) and generate a HDC using this DIB that it then passed to Graphics class. This instance of Graphics perfectly suits TextRenderer.DrawText. Of course you will need some DllImports particularly GetCurrentObject of gdi32.dll supplying the BufferedGraphics's Graphics pointer to native device context obtained by calling Graphics.GetHdc(). After drawing via BufferedGraphics graphics you must not call BufferedGraphics.Render() method as it will delete the HDC pointer and destroy of the Bitmap that is currently selected in it. Instead, obtain the pointer to the native HDC, call gdi32's GetCurrentObject and create a Bitmap through the pointer obtained from GetCurrentObject.

static Bitmap BitmapFromGraphics(this Graphics g, Rectangle r)
{
using (BufferedGraphics bg = BuffereGraphicsManager.Context.Allocate(g, r))
{
// make your drawing calls here
IntPtr hdc = bg.Graphics.GetHdc();
IntPtr bmp = GetCurrentObject(hdc, 7); // 7 is OBJ_BITMAP
Bitmap result = (Bitmap)Image.FromHBitmap(bmp);
bg.Graphics.ReleaseHdc(hdc)
return result;
}
}
Splashdown answered 13/4, 2024 at 10:42 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.