Search code examples
c#htmlimage-processingbitmaphtml-rendering

Render HTML as an Image on the backend and Convert to Base64 string


Entirely on the backend, with no console, no context, no session (part of an agent call that runs every few seconds); I need a way to convert either a small snippet of HTML, or an entire HTML document into an image (bitmap or otherwise) and then convert that into a base64 string so I can render the img into an email template.

The HTML itself is dynamic and the data within changes every time it is needed.

  • I have tried using different libraries like Aspose (https://products.aspose.com/html/net/) but it's not free and is quite slow at generation. Even for small snippets of HTML
  • I have tried using a default Webbrowser method. And this mostly works but does not render any CSS to go with the HTML. Inline or otherwise.

What is the simplest, quickest, easiest, way to render HTML with inline CSS into an image/bitmap/bitmapimage. Any external libraries/Nuget packages MUST be entirely free. The image then needs to be converted to a Base64 string. Auto cropping/auto sizing would also be a huge benefit to any answers.

So far this is the fastest and best I can do, but it fails at rendering the CSS for the HTML:

public static class UserDataExtensions
    {
        public static string SignBase64(this string base64, string mediaType, string charSet)
        {
            return "data:" + mediaType + ";charset=" + charSet + ";base64," + base64;
        }
    }

public class HtmlToImageConverter
        {
            private string _Html;
            private Bitmap _Image;
    
            private const string HTML_START = "<html><head></head><body>";
            private const string HTML_END = "</body></html>";
    
            public HtmlToImageConverter()
            {
            }
            public string ConvertHTML(string html)
            {
                _Html = HTML_START + html + HTML_END;
                return ToBase64(Render()).SignBase64("image/png", "utf-8");
            }
    
            private string ToBase64(Bitmap bitmap)
            {
                using (var memory = new MemoryStream())
                {
                    using (var newImage = new Bitmap(bitmap))
                    {
                        newImage.Save(memory, ImageFormat.Png);
                        var SigBase64 = Convert.ToBase64String(memory.GetBuffer()); // Get Base64
                        return SigBase64;
                    }
                }
            }
    
            private Bitmap Render()
            {
                var thread = new Thread(GenerateInternal);
                thread.SetApartmentState(ApartmentState.STA);
                thread.Start();
                thread.Join();
                return _Image;
            }
    
            private void GenerateInternal()
            {
                var webBrowser = new WebBrowser
                {
                    ScrollBarsEnabled = false,
                    DocumentText = _Html,
                    ClientSize = new Size(3000, 3000)
                };
    
                webBrowser.DocumentCompleted += WebBrowser_DocumentCompleted;
                while (webBrowser.ReadyState != WebBrowserReadyState.Complete) Application.DoEvents();
                webBrowser.Dispose();
            }
    
            private void WebBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
            {
                var webBrowser = (WebBrowser)sender;
    
                _Image = new Bitmap(webBrowser.Bounds.Width, webBrowser.Bounds.Height);
                webBrowser.BringToFront();
                webBrowser.DrawToBitmap(_Image, webBrowser.Bounds);
                _Image = AutoCrop(_Image);
            }
    
            private static byte[][] GetRgb(Bitmap bmp)
            {
                var bmpData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
                var ptr = bmpData.Scan0;
                var numPixels = bmp.Width * bmp.Height;
                var numBytes = bmpData.Stride * bmp.Height;
                var padding = bmpData.Stride - bmp.Width * 3;
                var i = 0;
                var ct = 1;
    
                var r = new byte[numPixels];
                var g = new byte[numPixels];
                var b = new byte[numPixels];
                var rgb = new byte[numBytes];
    
                Marshal.Copy(ptr, rgb, 0, numBytes);
    
                for (var x = 0; x < numBytes - 3; x += 3)
                {
                    if (x == (bmpData.Stride * ct - padding))
                    {
                        x += padding;
                        ct++;
                    }
    
                    r[i] = rgb[x];
                    g[i] = rgb[x + 1];
                    b[i] = rgb[x + 2]; i++;
                }
    
                bmp.UnlockBits(bmpData);
                return new[] { r, g, b };
            }
            private static Bitmap AutoCrop(Bitmap bmp)
            {
                // Get an array containing the R, G, B components of each pixel
                var pixels = GetRgb(bmp);
    
                var h = bmp.Height - 1;
                var w = bmp.Width;
                var top = 0;
                var bottom = h;
                var left = bmp.Width;
                var right = 0;
                var white = 0;
    
                const int tolerance = 95;
    
                var prevColor = false;
                for (var i = 0; i < pixels[0].Length; i++)
                {
                    int x = (i % (w)), y = (int)(Math.Floor((decimal)(i / w)));
                    const int tol = 255 * tolerance / 100;
                    if (pixels[0][i] >= tol && pixels[1][i] >= tol && pixels[2][i] >= tol)
                    {
                        white++;
                        right = (x > right && white == 1) ? x : right;
                    }
                    else
                    {
                        left = (x < left && white >= 1) ? x : left;
                        right = (x == w - 1 && white == 0) ? w - 1 : right;
                        white = 0;
                    }
    
                    if (white == w)
                    {
                        top = (y - top < 3) ? y : top;
                        bottom = (prevColor && x == w - 1 && y > top + 1) ? y : bottom;
                    }
    
                    left = (x == 0 && white == 0) ? 0 : left;
                    bottom = (y == h && x == w - 1 && white != w && prevColor) ? h + 1 : bottom;
    
                    if (x == w - 1)
                    {
                        prevColor = (white < w);
                        white = 0;
                    }
                }
    
                right = (right == 0) ? w : right;
                left = (left == w) ? 0 : left;
    
                // Cropy the image
                if (bottom - top > 0)
                {
                    return bmp.Clone(new Rectangle(left, top, right - left + 1, bottom - top), bmp.PixelFormat);
                }
    
                return bmp;
            }
        }

Solution

  • Sounds like you need a HTML renderer. I don't think there is a easy or simple way to do this, especially if there is dynamic content. You're best pick would be something like puppeteersharp

    public static void ExportPage() {
        await using var page = await browser.NewPageAsync();
        await page.SetContentAsync("<div>My Receipt</div>");
        var result = await page.GetContentAsync();
        page.ScreenshotAsync(myPath);
    }