【问题标题】:Render HTML as an Image on the backend and Convert to Base64 string在后端将 HTML 渲染为图像并转换为 Base64 字符串
【发布时间】:2022-09-23 06:54:22
【问题描述】:

完全在后端,没有控制台,没有上下文,没有会话(每隔几秒运行的代理调用的一部分);我需要一种方法将 HTML 的小 sn-p 或整个 HTML 文档转换为图像(位图或其他),然后将其转换为 base64 字符串,以便我可以将 img 呈现为电子邮件模板。

HTML 本身是动态的,并且每次需要时都会更改其中的数据。

  • 我尝试过使用不同的库,例如 Aspose (https://products.aspose.com/html/net/),但它不是免费的,而且生成速度很慢。即使对于 HTML 的小 sn-ps
  • 我已尝试使用默认的 Webbrowser 方法。这主要是有效的,但不会渲染任何与 HTML 一起使用的 CSS。内联或其他。

使用内联 CSS 将 HTML 渲染到图像/位图/位图图像中的最简单、最快、最简单的方法是什么。任何外部库/Nuget 包必须完全免费。 然后需要将图像转换为 Base64 字符串。 自动裁剪/自动调整大小也将对任何答案产生巨大的好处。

到目前为止,这是我能做的最快和最好的,但它无法为 HTML 渲染 CSS:

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;
            }
        }
  • 等等,您正在尝试将 HTML 呈现为图像,以嵌入到电子邮件中?为什么不将 HTML 作为电子邮件本身发送?每个现代电子邮件程序都会正确呈现它并将其显示给用户。
  • stackoverflow.com/a/60741246/14171304 ...但是为什么呢?如前所述发送 HTML。
  • @MindSwipe 不,不幸的是,我们的很多客户都使用 Outlook 电子邮件帐户。 Outlook 几乎不支持已有 10 年历史的 css/html 技术。我们想要使用的许多更好的样式在 Outlook 中不起作用。这个问题实际上是针对我们的用例的,因为我们公司发送给大量的老电子邮件用户。

标签: c# html image-processing bitmap html-rendering


【解决方案1】:

听起来你需要一个 HTML 渲染器。我认为没有一种简单或简单的方法可以做到这一点,尤其是在有动态内容的情况下。 你最好的选择是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);
}

【讨论】:

  • 这非常有效。尤其是木偶大师。太感谢了。我将使用 Puppeteersharp 发布我的完整转换器类作为另一个答案,但会将你的标记为解决方案:)
【解决方案2】:

再次感谢@MiVoth 的回答。使用 Puppeteersharp 就像一个魅力,并使用 CSS 正确呈现 HTML。 我的旧解决方案可以很好地渲染 HTML,但没有等待 CSS 加载。

以下解决方案完美地呈现了我传递给它的任何 HTML,并自动裁剪 HTML 以消除空白。如果您自定义设置宽度和高度,它将自动将呈现的 HTML 居中在白色背景上:

using PuppeteerSharp;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;


namespace spacename
{
    public class HtmlToImageConverter
    {
        private string _Html { get; set; }
        private Bitmap _Image { get; set; }
        private Browser _Browser { get; set; }
        private Page _Page { get; set; }

        private const string HTML_DEFAULT_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
        private const string HTML_DEFAULT_HTML = "<html lang=\"en\" xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:v=\"urn:schemas-microsoft-com:vml\" style=\"-ms-text-size-adjust: 100%; margin: 0 auto; padding: 0; height: 100%; width: 100%;\">\r\n##HEADER_CONTENT##\r\n##BODY_CONTENT##\r\n</html>";
        private const string HTML_DEFAULT_HEADER = "<head>##CONTENT##</head>";
        private const string HTML_DEFAULT_BODY = "<body width=\"100%\" style=\"-ms-text-size-adjust: 100%; mso-line-height-rule: exactly; margin: 0 auto; padding: 0; height: 100%; width: 100%;\">\r\n##CONTENT##\r\n</body>";

        private const int MAX_WIDTH = 3000;
        private const int MAX_HEIGHT = 3000;
        private int? _Width { get; set; } = null;
        private int? _Height { get; set; } = null;
        private bool _CompleteHTML { get; set; }

        #region Intialize
        public static HtmlToImageConverter CreateSync()
        {
            HtmlToImageConverter obj;
            var task = Task.Run(async () => await CreateAsync());
            task.Wait();
            obj = task.Result;
            return obj;
        }
        public static async Task<HtmlToImageConverter> CreateAsync()
        {
            HtmlToImageConverter obj = new HtmlToImageConverter();
            await obj.InitializeAsync();
            return obj;
        }
        public async Task<bool> DestroyAsync()
        {
            return await DisposeAsync();
        }
        public bool DestroySync()
        {
            var task = Task.Run(async () => await DisposeAsync());
            task.Wait();
            return task.Result;
        }
        private HtmlToImageConverter() { }
        #endregion

        #region Main Functions
        /// <summary>
        /// Set Object Values.<br />
        /// _Html = build HTML via <paramref name="completeHTML"/><br />
        /// _Width = <paramref name="width"/>.Value<br />
        /// _Height = <paramref name="completeHTML"/>.Value<br />
        /// </summary>
        /// <param name="html"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="completeHTML"></param>
        public void LoadHtml(string html, string htmlHeaderContent, int? width, int? height, bool completeHTML = false, string customHtmlStart = "", string customHtmlEnd = "")
        {
            _CompleteHTML = completeHTML;

            StringBuilder str = new StringBuilder();
            str.Append(HTML_DEFAULT_DOCTYPE);
            str.Append(HTML_DEFAULT_HTML);
            str.Replace("##HEADER_CONTENT##", HTML_DEFAULT_HEADER);
            str.Replace("##CONTENT##", htmlHeaderContent);
            str.Replace("##BODY_CONTENT##", HTML_DEFAULT_BODY);

            // Set _Html
            if (!_CompleteHTML)
            {
                if (customHtmlStart != string.Empty && customHtmlEnd != string.Empty)
                {
                    _Html = customHtmlStart + html + customHtmlEnd;
                }
                else
                {
                    str.Replace("##CONTENT##", html);
                    _Html = str.ToString();
                }
            }
            else _Html = html;

            if (width != null) _Width = width.Value;
            if (height != null) _Height = height.Value;
        }
        public string ConvertHTMLSync(ImageFormat imgFormat)
        {
            var task = Task.Run(async () => await ConvertHTMLAsync(imgFormat));
            task.Wait();
            return task.Result;
        }
        public async Task<string> ConvertHTMLAsync(ImageFormat imgFormat, string htmlHeaderContent = "")
        {
            string base64 = string.Empty;

            try
            {
                // Reload the browser and page if they're closed
                if (_Browser.IsClosed || _Page.IsClosed) await InitializeAsync();

                // Set content and viewport size
                await _Page.SetContentAsync(_Html);
                await _Page.SetViewportAsync(new ViewPortOptions
                {
                    Width = MAX_WIDTH,
                    Height = MAX_HEIGHT
                });
                // Take screenshot and save results as Stream
                var result = await _Page.ScreenshotStreamAsync();
                using (System.Drawing.Image img = System.Drawing.Image.FromStream(result))
                {
                    // Set _Image to new Bitmap of img size and get graphical renderer from Bitmap
                    _Image = new Bitmap(img.Width, img.Height);
                    using (var graphics = Graphics.FromImage(_Image))
                    {
                        // Draw the image to the Bitmap
                        graphics.DrawImage(img, 0, 0, img.Width, img.Height);
                    }
                }

                // Auto crop with auto centering
                _Image = _Image.AutoCrop(_Width, _Height);
                base64 = _Image.ToBase64(imgFormat);
            }
            catch (Exception ex)
            {
                throw ex;
            }

            // Return signed base64 image string or full HTML with image as body
            if (!_CompleteHTML)
                return base64.SignBase64(imgFormat.ImageFormatToString(), "utf-8");
            else
            {
                StringBuilder str = new StringBuilder();
                str.Append(HTML_DEFAULT_DOCTYPE);
                str.Append(HTML_DEFAULT_HTML);
                str.Replace("##HEADER_CONTENT##", HTML_DEFAULT_HEADER);
                str.Replace("##CONTENT##", htmlHeaderContent);
                str.Replace("##BODY_CONTENT##", HTML_DEFAULT_BODY);

                str.Replace("##CONTENT##", base64.SignBase64(imgFormat.ImageFormatToString(), "utf-8").TagSignBase64("Product Partner Rendered Email"));
                return str.ToString();
            }
        }
        #endregion

        #region Create / Destroy
        /// <summary>
        /// Intialize the _Browser and _Page for later use
        /// </summary>
        /// <returns></returns>
        private async Task InitializeAsync()
        {
            using (var browserFetcher = new BrowserFetcher())
            {
                await browserFetcher.DownloadAsync();
                _Browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
                _Page = await _Browser.NewPageAsync();
            }
        }
        /// <summary>
        /// Destroy the _Browser and _Page async objects
        /// </summary>
        /// <returns></returns>
        private async Task<bool> DisposeAsync()
        {
            await _Browser.DisposeAsync();
            await _Page.DisposeAsync();
            return (!_Browser.IsClosed) && (!_Page.IsClosed);
        }
        #endregion
    }

    public static class ConverterExtensions
    {
        /// <summary>
        /// Convert <paramref name="bitmap"/> to Base64 string.<br />
        /// Format image via <paramref name="imgFormat"/>
        /// </summary>
        /// <param name="bitmap"></param>
        /// <param name="imgFormat"></param>
        /// <returns></returns>
        public static string ToBase64(this Bitmap bitmap, ImageFormat imgFormat)
        {
            using (var memory = new MemoryStream())
            {
                using (var newImage = new Bitmap(bitmap))
                {
                    newImage.Save(memory, imgFormat);
                    // Get base64
                    var SigBase64 = Convert.ToBase64String(memory.GetBuffer());
                    return SigBase64;
                }
            }
        }
        /// <summary>
        /// Get array of all pixel colours by row/column from <paramref name="bmp"/> Bitmap
        /// </summary>
        /// <param name="bmp"></param>
        /// <returns></returns>
        public static byte[][] GetRgb(this 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 };
        }
        /// <summary>
        /// Automatically Crop by any White pixels on <paramref name="bmp"/> Bitmap.<br />
        /// Cap the width of the resulting Bitmap via <paramref name="Cap_Width"/><br />
        /// Cap the height of the resulting Bitmap via <paramref name="Cap_Height"/>
        /// </summary>
        /// <param name="bmp"></param>
        /// <param name="Cap_Width"></param>
        /// <param name="Cap_Height"></param>
        /// <returns></returns>
        public static Bitmap AutoCrop(this Bitmap bmp, int? Cap_Width, int? Cap_Height)
        {
            // Get an array containing the R, G, B components of each pixel
            var pixels = bmp.GetRgb();

            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 = 100;

            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;

            // Cap minimum values to set _Width and _Height
            var SetWidth = right - left;
            SetWidth = (Cap_Width == null ? SetWidth : (SetWidth >= Cap_Width.Value ? SetWidth : Cap_Width.Value));
            var SetHeight = bottom - top;
            SetHeight = (Cap_Height == null ? SetHeight : (SetHeight >= Cap_Height.Value ? SetHeight : Cap_Height.Value));
            // Cap left and right
            var CenteredLeft = left + (((right - SetWidth) - left) / 2);
            var CenteredTop = (top + ((bottom - SetHeight) - top) / 2);

            // Cropy the image
            if (bottom - top > 0)
            {
                return bmp.Clone(new Rectangle(CenteredLeft, CenteredTop, SetWidth, SetHeight), bmp.PixelFormat);
            }

            return bmp;
        }
        /// <summary>
        /// Sign a given <paramref name="base64"/> string with valid <paramref name="mediaType"/> and <paramref name="charSet"/>
        /// </summary>
        /// <param name="base64"></param>
        /// <param name="mediaType"></param>
        /// <param name="charSet"></param>
        /// <returns></returns>
        public static string SignBase64(this string base64, string mediaType, string charSet)
        {
            return "data:" + mediaType + ";charset=" + charSet + ";base64," + base64;
        }
        /// <summary>
        /// HTML Tag a given signed <paramref name="SignedBase64String"/> string.<br />
        /// Set the img alt text to <paramref name="alt"/> and set any <paramref name="customCss"/> on img.<br />
        /// Set width and height via <paramref name="width"/> and <paramref name="height"/>
        /// </summary>
        /// <param name="SignedBase64String"></param>
        /// <param name="alt"></param>
        /// <param name="customCss"></param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <returns></returns>
        public static string TagSignBase64(this string SignedBase64String, string alt, string customCss = "", string width = "100%", string height = "auto")
        {
            return "<img" +
                (customCss != string.Empty ? " style=\"" + customCss + "\"" : "") +
                " src=\"" + SignedBase64String +
                "\" alt=\"" + alt +
                "\" width=\"" + width +
                ";\" height=\"" + height +
                ";\"/>";
        }
        /// <summary>
        /// Converts given <paramref name="imgFormat"/> to image/type
        /// </summary>
        /// <param name="imgFormat"></param>
        /// <returns></returns>
        public static string ImageFormatToString(this ImageFormat imgFormat)
        {
            StringBuilder mediaType = new StringBuilder();
            mediaType.Append("image/");

            if (imgFormat == ImageFormat.MemoryBmp || imgFormat == ImageFormat.Bmp)
                mediaType.Append("bmp");
            if (imgFormat == ImageFormat.Emf)
                mediaType.Append("jpeg");
            if (imgFormat == ImageFormat.Wmf)
                mediaType.Append("x-wmf");
            if (imgFormat == ImageFormat.Gif)
                mediaType.Append("gif");
            if (imgFormat == ImageFormat.Jpeg)
                mediaType.Append("jpeg");
            if (imgFormat == ImageFormat.Png)
                mediaType.Append("png");
            if (imgFormat == ImageFormat.Tiff)
                mediaType.Append("tiff");
            if (imgFormat == ImageFormat.Exif)
                mediaType.Append("jpeg");
            if (imgFormat == ImageFormat.Icon)
                mediaType.Append("x-icon");

            return mediaType.ToString();
        }
    }
}

【讨论】:

    猜你喜欢
    • 2018-09-07
    • 2014-07-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-14
    相关资源
    最近更新 更多