From 33468039a10ba00485618517883a8f036de9f0b6 Mon Sep 17 00:00:00 2001 From: Alby Blyth Date: Thu, 4 Apr 2024 13:12:57 +1000 Subject: [PATCH] Add image generator --- .../SampleConverterBase.cs | 4 +- .../HtmlRenderer.Demo.Console/Program.cs | 8 +- .../{SkiaConverter.cs => SkiaPdfConverter.cs} | 4 +- .../SkiaSvgConverter.cs | 36 ++++ .../HtmlRenderer.SkiaSharp/ImageGenerator.cs | 160 ++++++++++++++++++ 5 files changed, 205 insertions(+), 7 deletions(-) rename Source/Demos/HtmlRenderer.Demo.Console/{SkiaConverter.cs => SkiaPdfConverter.cs} (83%) create mode 100644 Source/Demos/HtmlRenderer.Demo.Console/SkiaSvgConverter.cs create mode 100644 Source/HtmlRenderer.SkiaSharp/ImageGenerator.cs diff --git a/Source/Demos/HtmlRenderer.Demo.Common/SampleConverterBase.cs b/Source/Demos/HtmlRenderer.Demo.Common/SampleConverterBase.cs index ed5370c0d..6556fbad1 100644 --- a/Source/Demos/HtmlRenderer.Demo.Common/SampleConverterBase.cs +++ b/Source/Demos/HtmlRenderer.Demo.Common/SampleConverterBase.cs @@ -55,11 +55,11 @@ public SampleConverterFileBase(string sampleRunIdentifier, string basePath) : ba _thisTypeName = this.GetType().Name; } - protected string GetSamplePath(HtmlSample sample) + protected string GetSamplePath(HtmlSample sample, string extension = ".pdf") { var path = Path.Combine(_basePath, _sampleRunIdentifier); Directory.CreateDirectory(path); - return Path.Combine(path, sample.FullName + _thisTypeName + "_" + ".pdf"); + return Path.Combine(path, sample.FullName + _thisTypeName + "_" + extension); } } } diff --git a/Source/Demos/HtmlRenderer.Demo.Console/Program.cs b/Source/Demos/HtmlRenderer.Demo.Console/Program.cs index 186bc09a2..81d20d46a 100644 --- a/Source/Demos/HtmlRenderer.Demo.Console/Program.cs +++ b/Source/Demos/HtmlRenderer.Demo.Console/Program.cs @@ -13,7 +13,8 @@ //Probably won't be running a suite of tests more than once a second, so this will do. var runIdentifier = DateTime.Now.ToString("yyyyMMdd-hhmmss"); -var skia = new SkiaConverter(runIdentifier, basePath); +var skia = new SkiaPdfConverter(runIdentifier, basePath); +var svgSkia = new SkiaSvgConverter(runIdentifier, basePath); var pdfSharp = new PdfSharpCoreConverter(runIdentifier, basePath); SamplesLoader.Init("Console", typeof(Program).Assembly.GetName().Version.ToString()); @@ -23,10 +24,11 @@ foreach (var htmlSample in samples) { ////Just doing one test here. Comment this for all of them. - //if (!htmlSample.FullName.Contains("16", StringComparison.OrdinalIgnoreCase)) continue; + if (!htmlSample.FullName.Contains("16", StringComparison.OrdinalIgnoreCase)) continue; await skia.GenerateSampleAsync(htmlSample); - await pdfSharp.GenerateSampleAsync(htmlSample); + await svgSkia.GenerateSampleAsync(htmlSample); + //await pdfSharp.GenerateSampleAsync(htmlSample); } diff --git a/Source/Demos/HtmlRenderer.Demo.Console/SkiaConverter.cs b/Source/Demos/HtmlRenderer.Demo.Console/SkiaPdfConverter.cs similarity index 83% rename from Source/Demos/HtmlRenderer.Demo.Console/SkiaConverter.cs rename to Source/Demos/HtmlRenderer.Demo.Console/SkiaPdfConverter.cs index 5cfa068a4..0e3c4de30 100644 --- a/Source/Demos/HtmlRenderer.Demo.Console/SkiaConverter.cs +++ b/Source/Demos/HtmlRenderer.Demo.Console/SkiaPdfConverter.cs @@ -9,9 +9,9 @@ namespace HtmlRenderer.Demo.Console { - public class SkiaConverter : SampleConverterFileBase + public class SkiaPdfConverter : SampleConverterFileBase { - public SkiaConverter(string sampleRunIdentifier, string basePath) : base(sampleRunIdentifier, basePath) + public SkiaPdfConverter(string sampleRunIdentifier, string basePath) : base(sampleRunIdentifier, basePath) { } diff --git a/Source/Demos/HtmlRenderer.Demo.Console/SkiaSvgConverter.cs b/Source/Demos/HtmlRenderer.Demo.Console/SkiaSvgConverter.cs new file mode 100644 index 000000000..a9a897c19 --- /dev/null +++ b/Source/Demos/HtmlRenderer.Demo.Console/SkiaSvgConverter.cs @@ -0,0 +1,36 @@ +using HtmlRenderer.Demo.Common; +using SkiaSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TheArtOfDev.HtmlRenderer.Demo.Common; +using TheArtOfDev.HtmlRenderer.SkiaSharp; + +namespace HtmlRenderer.Demo.Console +{ + public class SkiaSvgConverter : SampleConverterFileBase + { + public SkiaSvgConverter(string sampleRunIdentifier, string basePath) : base(sampleRunIdentifier, basePath) + { + } + + public async Task GenerateSampleAsync(HtmlSample sample) + { + var size = new SKSize(500, 1000); + + using (var fileStream = File.Open(GetSamplePath(sample, ".svg"), FileMode.CreateNew)) + { + await ImageGenerator.GenerateSvgAsync(sample.Html, fileStream, size, imageLoad: OnImageLoaded); + fileStream.Flush(); + } + + using (var fileStream = File.Open(GetSamplePath(sample, ".png"), FileMode.CreateNew)) + { + await ImageGenerator.GenerateBitmapAsync(sample.Html, fileStream, size, SKEncodedImageFormat.Png, 100, imageLoad: OnImageLoaded); + fileStream.Flush(); + } + } + } +} diff --git a/Source/HtmlRenderer.SkiaSharp/ImageGenerator.cs b/Source/HtmlRenderer.SkiaSharp/ImageGenerator.cs new file mode 100644 index 000000000..2add0b57c --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/ImageGenerator.cs @@ -0,0 +1,160 @@ +// "Therefore those skilled at the unorthodox +// are infinite as heaven and earth, +// inexhaustible as the great rivers. +// When they come to an end, +// they begin again, +// like the days and months; +// they die and are reborn, +// like the four seasons." +// +// - Sun Tsu, +// "The Art of War" + +using HtmlRenderer.Core.Dom; +using SkiaSharp; +using Svg.Skia; +using System; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters; +using TheArtOfDev.HtmlRenderer.SkiaSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp +{ + /// + /// TODO:a add doc + /// + public static class ImageGenerator + { + /// + /// Adds a font mapping from to iff the is not found.
+ /// When the font is used in rendered html and is not found in existing + /// fonts (installed or added) it will be replaced by .
+ ///
+ /// + /// This fonts mapping can be used as a fallback in case the requested font is not installed in the client system. + /// + /// the font family to replace + /// the font family to replace with + public static void AddFontFamilyMapping(string fromFamily, string toFamily) + { + ArgChecker.AssertArgNotNullOrEmpty(fromFamily, "fromFamily"); + ArgChecker.AssertArgNotNullOrEmpty(toFamily, "toFamily"); + + SkiaSharpAdapter.Instance.AddFontFamilyMapping(fromFamily, toFamily); + } + + /// + /// Parse the given stylesheet to object.
+ /// If is true the parsed css blocks are added to the + /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned. + ///
+ /// + /// the stylesheet source to parse + /// true - combine the parsed css data with default css data, false - return only the parsed css data + /// the parsed css data + public static CssData ParseStyleSheet(string stylesheet, bool combineWithDefault = true) + { + return CssData.Parse(SkiaSharpAdapter.Instance, stylesheet, combineWithDefault); + } + + /// + /// Create Svg document from given HTML.
+ ///
+ /// HTML source to create image from + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static async Task GenerateSvgAsync( + string html, + Stream outputStream, + SKSize size, + CssData cssData = null, + EventHandler stylesheetLoad = null, + EventHandler imageLoad = null) + { + // create svg document to render the HTML into + var canvas = SKSvgCanvas.Create(new SKRect(0, 0, size.Width, size.Height), outputStream); + + // add rendered image + await DrawSvgAsync(canvas, html, size, cssData, stylesheetLoad, imageLoad); + canvas.Dispose(); + + return canvas; + } + + /// + /// Writes html to a bitmap image + /// + /// HTML source to create image from + /// The size of the image + /// The file format used to encode the image. + /// The quality level to use for the image. Quality range from 0-100. Higher values correspond to improved visual quality, but less compression. + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// + public static async Task GenerateBitmapAsync( + string html, + Stream outputStream, + SKSize size, + SKEncodedImageFormat imageFormat, + int quality, + CssData cssData = null, + EventHandler stylesheetLoad = null, + EventHandler imageLoad = null) + { + + var bitmap = new SKBitmap((int)size.Width, (int)size.Height); + var canvas = new SKCanvas(bitmap); + + // add rendered image + await DrawSvgAsync(canvas, html, size, cssData, stylesheetLoad, imageLoad); + bitmap.Encode(outputStream, imageFormat, quality); + + return canvas; + } + + /// + /// Create image pages from given HTML and appends them to the provided image document.
+ ///
+ /// canvas to draw to + /// HTML source to create image from + /// optional: the style to use for html rendering (default - use W3 default style) + /// optional: can be used to overwrite stylesheet resolution logic + /// optional: can be used to overwrite image resolution logic + /// the generated image of the html + public static async Task DrawSvgAsync( + SKCanvas canvas, + string html, + SKSize size, + CssData cssData = null, + EventHandler stylesheetLoad = null, + EventHandler imageLoad = null) + { + using var container = new HtmlContainer(); + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + + container.Location = new SKPoint(0, 0); + //container.MaxSize = size; + container.MaxSize = new SKSize(size.Width, 0); + container.PageSize = size; + container.MarginBottom = 0; + container.MarginLeft = 0; + container.MarginRight = 0; + container.MarginTop = 0; + container.ScrollOffset = new SKPoint(0, 0); + + await container.SetHtml(html, cssData); + + // layout the HTML with the page width restriction to know how many pages are required + await container.PerformLayout(canvas); + await container.PerformPaint(canvas); + } + } +} \ No newline at end of file