diff --git a/.gitignore b/.gitignore index 31fb5503a..2b7fc1ae1 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,4 @@ UpgradeLog*.XML *.DotSettings /Source/.vs/ +/Source/Demo/Console/HtmlRenderer.Demo.Console/Output diff --git a/Source/Demo/Common/TestSamples/06.External Image.htm b/Source/Demo/Common/TestSamples/06.External Image.htm index 0492a426c..70f3a6969 100644 --- a/Source/Demo/Common/TestSamples/06.External Image.htm +++ b/Source/Demo/Common/TestSamples/06.External Image.htm @@ -8,7 +8,7 @@
From web:
- +
GIF: @@ -21,6 +21,6 @@

- + \ No newline at end of file diff --git a/Source/Demo/Common/TestSamples/15.MaxWidth.htm b/Source/Demo/Common/TestSamples/15.MaxWidth.htm index d8b98e9ba..9b47aace3 100644 --- a/Source/Demo/Common/TestSamples/15.MaxWidth.htm +++ b/Source/Demo/Common/TestSamples/15.MaxWidth.htm @@ -26,10 +26,10 @@

metus. Integer leo dolor, tristique a, dignissim ac, iaculis eget, elit. Donec arcu.

The image should also be limited by size because it has: style="width:90%"
- - + +

- +

diff --git a/Source/Demo/Common/TestSamples/19.Many images.htm b/Source/Demo/Common/TestSamples/19.Many images.htm index a7cd2582c..f3afe3e9f 100644 --- a/Source/Demo/Common/TestSamples/19.Many images.htm +++ b/Source/Demo/Common/TestSamples/19.Many images.htm @@ -2,102 +2,102 @@

Contains many images that should not load until in scroll view

Image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- +

Another image

- + \ No newline at end of file diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/HtmlRenderer.Demo.Console.csproj b/Source/Demo/Console/HtmlRenderer.Demo.Console/HtmlRenderer.Demo.Console.csproj new file mode 100644 index 000000000..7bf635e67 --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/HtmlRenderer.Demo.Console.csproj @@ -0,0 +1,17 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/PdfSharpCoreConverter.cs b/Source/Demo/Console/HtmlRenderer.Demo.Console/PdfSharpCoreConverter.cs new file mode 100644 index 000000000..98409ecbc --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/PdfSharpCoreConverter.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TheArtOfDev.HtmlRenderer.Demo.Common; +using TheArtOfDev.HtmlRenderer.PdfSharp; + +namespace HtmlRenderer.Demo.Console +{ + public class PdfSharpCoreConverter : SampleConverterBase + { + public PdfSharpCoreConverter(string sampleRunIdentifier, string basePath) : base(sampleRunIdentifier, basePath) + { + } + + public void GenerateSample(HtmlSample sample) + { + var config = new PdfGenerateConfig(); + + config.PageSize = PdfSharpCore.PageSize.A4; + config.MarginLeft = 0; + config.MarginRight = 0; + config.MarginTop = 0; + config.MarginBottom = 0; + + var pdf = PdfGenerator.GeneratePdf(sample.Html, config, base.CssData, base.StylesheetLoad, base.ImageLoad); + pdf.Save(GetSamplePath(sample)); + } + } +} diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/Program.cs b/Source/Demo/Console/HtmlRenderer.Demo.Console/Program.cs new file mode 100644 index 000000000..1188705bb --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/Program.cs @@ -0,0 +1,30 @@ +using HtmlRenderer.Demo.Console; +using System.Diagnostics; +using TheArtOfDev.HtmlRenderer.Demo.Common; + +//By default, write to a sub folder 'output' +string basePath= @".\Ouput"; +if (args.Length > 0) +{ + //And if there's an output path given, use that. + basePath = args[0]; +} + +//Probably won't be running a suite of tests more than once a second, so this will do. +string runIdentifier = DateTime.Now.ToString("ddMMyyyy-hhMMss"); + +var skia = new SkiaConverter(runIdentifier, basePath); +var pdfSharp = new PdfSharpCoreConverter(runIdentifier, basePath); + +SamplesLoader.Init("Console", typeof(Program).Assembly.GetName().Version.ToString()); + +var samples = SamplesLoader.TestSamples; + +foreach (var htmlSample in samples) +{ + skia.GenerateSample(htmlSample); + pdfSharp.GenerateSample(htmlSample); +} + + +//At this point.. there should be something!! \ No newline at end of file diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/Properties/launchSettings.json b/Source/Demo/Console/HtmlRenderer.Demo.Console/Properties/launchSettings.json new file mode 100644 index 000000000..6deb4f2a6 --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "HtmlRenderer.Demo.Console": { + "commandName": "Project", + "commandLineArgs": "..\\..\\..\\Output" + } + } +} \ No newline at end of file diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/SampleConverterBase.cs b/Source/Demo/Console/HtmlRenderer.Demo.Console/SampleConverterBase.cs new file mode 100644 index 000000000..747b8ba42 --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/SampleConverterBase.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using TheArtOfDev.HtmlRenderer.Demo.Common; + +namespace HtmlRenderer.Demo.Console +{ + public class SampleConverterBase + { + private string _sampleRunIdentifier; + private string _thisTypeName; + private string _basePath; + + public SampleConverterBase(string sampleRunIdentifier, string basePath) + { + _sampleRunIdentifier = sampleRunIdentifier; + _basePath = basePath; + _thisTypeName = this.GetType().Name; + } + + protected string GetSamplePath(HtmlSample sample) + { + var path = Path.Combine(_basePath, _sampleRunIdentifier); + Directory.CreateDirectory(path); + return Path.Combine(path, sample.FullName + _thisTypeName + "_" + ".pdf"); + } + } +} diff --git a/Source/Demo/Console/HtmlRenderer.Demo.Console/SkiaConverter.cs b/Source/Demo/Console/HtmlRenderer.Demo.Console/SkiaConverter.cs new file mode 100644 index 000000000..6b33326c0 --- /dev/null +++ b/Source/Demo/Console/HtmlRenderer.Demo.Console/SkiaConverter.cs @@ -0,0 +1,36 @@ +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 SkiaConverter : SampleConverterBase + { + public SkiaConverter(string sampleRunIdentifier, string basePath) : base(sampleRunIdentifier, basePath) + { + } + + public void GenerateSample(HtmlSample sample) + { + var config = new PdfGenerateConfig(); + + config.PageSize = PageSizeType.A4; + //TODO: 'Units' on config, rather than this c/p 🤮 + config.MarginLeft = PdfGenerateConfig.MilimitersToPixels(0); + config.MarginRight = PdfGenerateConfig.MilimitersToPixels(0); + config.MarginTop = PdfGenerateConfig.MilimitersToPixels(0); + config.MarginBottom = PdfGenerateConfig.MilimitersToPixels(0); + + var pdf = PdfGenerator.GeneratePdf(sample.Html, config, base.CssData, base.StylesheetLoad, base.ImageLoad); + using (var fileStream = File.Open(GetSamplePath(sample), FileMode.CreateNew)) + { + pdf.CopyTo(fileStream); + fileStream.Flush(); + } + } + } +} diff --git a/Source/Demo/WPF/HtmlRenderer.Demo.WPF.csproj b/Source/Demo/WPF/HtmlRenderer.Demo.WPF.csproj index 8c26cdcc4..d23e99825 100644 --- a/Source/Demo/WPF/HtmlRenderer.Demo.WPF.csproj +++ b/Source/Demo/WPF/HtmlRenderer.Demo.WPF.csproj @@ -1,5 +1,5 @@  - + Debug @@ -9,7 +9,7 @@ Properties TheArtOfDev.HtmlRenderer.Demo.WPF HtmlRendererWpfDemo - v4.0 + v4.8 512 {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 @@ -26,6 +26,7 @@ DEBUG;TRACE prompt 4 + false AnyCPU @@ -35,6 +36,7 @@ TRACE prompt 4 + false html.ico @@ -122,6 +124,7 @@ + diff --git a/Source/Demo/WPF/app.config b/Source/Demo/WPF/app.config new file mode 100644 index 000000000..3e0e37cfc --- /dev/null +++ b/Source/Demo/WPF/app.config @@ -0,0 +1,3 @@ + + + diff --git a/Source/HtmlRenderer.Core/Adapters/RAdapter.cs b/Source/HtmlRenderer.Core/Adapters/RAdapter.cs index 6b13459c1..942604de2 100644 --- a/Source/HtmlRenderer.Core/Adapters/RAdapter.cs +++ b/Source/HtmlRenderer.Core/Adapters/RAdapter.cs @@ -213,7 +213,7 @@ public RImage GetLoadingImage() { if (_loadImage == null) { - var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("TheArtOfDev.HtmlRenderer.Core.Utils.ImageLoad.png"); + var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("HtmlRenderer.Core.Core.Utils.ImageLoad.png"); if (stream != null) _loadImage = ImageFromStream(stream); } @@ -227,7 +227,7 @@ public RImage GetLoadingFailedImage() { if (_errorImage == null) { - var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("TheArtOfDev.HtmlRenderer.Core.Utils.ImageError.png"); + var stream = typeof(HtmlRendererUtils).Assembly.GetManifestResourceStream("HtmlRenderer.Core.Core.Utils.ImageError.png"); if (stream != null) _errorImage = ImageFromStream(stream); } diff --git a/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj b/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj index 86ea3bbe7..d0bbe4265 100644 --- a/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj +++ b/Source/HtmlRenderer.Core/HtmlRenderer.Core.csproj @@ -4,4 +4,14 @@ netcoreapp2.1 + + + + + + + + + + diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/BrushAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/BrushAdapter.cs new file mode 100644 index 000000000..645ef8621 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/BrushAdapter.cs @@ -0,0 +1,49 @@ +// "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 System; +using TheArtOfDev.HtmlRenderer.Adapters; +using SkiaSharp; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms brushes objects for core. + /// + internal sealed class BrushAdapter : RBrush + { + /// + /// The actual PdfSharp brush instance.
+ /// Should be but there is some fucking issue inheriting from it =/ + ///
+ private readonly object _brush; + + /// + /// Init. + /// + public BrushAdapter(object brush) + { + _brush = brush; + } + + /// + /// The actual WinForms brush instance. + /// + public object Brush + { + get { return _brush; } + } + + public override void Dispose() + { } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/FontAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/FontAdapter.cs new file mode 100644 index 000000000..c78110265 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/FontAdapter.cs @@ -0,0 +1,105 @@ +// "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 TheArtOfDev.HtmlRenderer.Adapters; +using SkiaSharp; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms Font object for core. + /// + internal sealed class FontAdapter : RFont + { + #region Fields and Consts + + /// + /// the underline win-forms font. + /// + private readonly SKFont _font; + + /// + /// the vertical offset of the font underline location from the top of the font. + /// + private double _underlineOffset = -1; + + /// + /// Cached font height. + /// + private double _height = -1; + + /// + /// Cached font whitespace width. + /// + private double _whitespaceWidth = -1; + + #endregion + + + /// + /// Init. + /// + public FontAdapter(SKFont font) + { + _font = font; + } + + /// + /// the underline win-forms font. + /// + public SKFont Font + { + get { return _font; } + } + + public override double Size + { + get { return _font.Size; } + } + + public override double UnderlineOffset + { + get { return _underlineOffset; } + } + + public override double Height + { + get { return _height; } + } + + public override double LeftPadding + { + get { return _height / 6f; } + } + + + public override double GetWhitespaceWidth(RGraphics graphics) + { + if (_whitespaceWidth < 0) + { + _whitespaceWidth = graphics.MeasureString(" ", this).Width; + } + return _whitespaceWidth; + } + + /// + /// Set font metrics to be cached for the font for future use. + /// + /// the full height of the font + /// the vertical offset of the font underline location from the top of the font. + internal void SetMetrics(int height, int underlineOffset) + { + _height = height; + _underlineOffset = underlineOffset; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/FontFamilyAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/FontFamilyAdapter.cs new file mode 100644 index 000000000..28acec93e --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/FontFamilyAdapter.cs @@ -0,0 +1,49 @@ +// "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 SkiaSharp; +using TheArtOfDev.HtmlRenderer.Adapters; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms Font object for core. + /// + internal sealed class FontFamilyAdapter : RFontFamily + { + /// + /// the underline win-forms font. + /// + private readonly SKTypeface _fontFamily; + + /// + /// Init. + /// + public FontFamilyAdapter(SKTypeface fontFamily) + { + _fontFamily = fontFamily; + } + + /// + /// the underline win-forms font family. + /// + public SKTypeface FontFamily + { + get { return _fontFamily; } + } + + public override string Name + { + get { return _fontFamily.FamilyName; } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsAdapter.cs new file mode 100644 index 000000000..708de75f0 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsAdapter.cs @@ -0,0 +1,259 @@ +// "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 SkiaSharp; +using System; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.SkiaSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms Graphics for core. + /// + internal sealed class GraphicsAdapter : RGraphics + { + #region Fields and Consts + + /// + /// The wrapped WinForms graphics object + /// + private readonly SKCanvas _canvas; + private readonly SKBitmap _bitmap; + + /// + /// if to release the graphics object on dispose + /// + private readonly bool _releaseGraphics; + + /// + /// Used to measure and draw strings + /// + private static readonly SKPaint _stringFormat; + + #endregion + + + static GraphicsAdapter() + { + _stringFormat = new SKPaint(); + _stringFormat.TextAlign = SKTextAlign.Left; + } + + /// + /// Init. + /// + /// the win forms graphics object to use + /// optional: if to release the graphics object on dispose (default - false) + public GraphicsAdapter(SKCanvas g, SKBitmap bitmap, bool releaseGraphics = false) + : base(SkiaSharpAdapter.Instance, new RRect(0, 0, double.MaxValue, double.MaxValue)) + { + ArgChecker.AssertArgNotNull(g, "g"); + + _canvas = g; + _bitmap = bitmap; + + _releaseGraphics = releaseGraphics; + } + + public override void PopClip() + { + _clipStack.Pop(); + _canvas.Restore(); + } + + public override void PushClip(RRect rect) + { + _clipStack.Push(rect); + _canvas.Save(); + _canvas.ClipRect(Utils.Convert(rect)); + } + + public override void PushClipExclude(RRect rect) + { } + + bool _antiAlias = true; + public override Object SetAntiAliasSmoothingMode() + { + var prevMode = _antiAlias; + _antiAlias = true; + return prevMode; + //var prevMode = _g.SmoothingMode; + //_g.SmoothingMode = XSmoothingMode.AntiAlias; + //return prevMode; + } + + public override void ReturnPreviousSmoothingMode(Object prevMode) + { + if (prevMode != null) + { + _antiAlias = (bool)prevMode; + } + } + + public override RSize MeasureString(string str, RFont font) + { + var fontAdapter = (FontAdapter)font; + var realFont = fontAdapter.Font; + + var sf = new SKPaint(realFont); + + //TODO: need to restrict this skpaint to the width of the canvas. + var boundingRect = new SKRect(); + + var width = sf.MeasureText(str, ref boundingRect); + var height = -realFont.Metrics.Top + realFont.Metrics.Descent; + //var size = _g.MeasureString(str, realFont, _stringFormat); + + //The returned width includes whitespace, which we want. The bounding rect will exclude white space. + var size = new SKSize(width, height); + + if (font.Height < 0) + { + fontAdapter.SetMetrics((int)Math.Round(height, MidpointRounding.AwayFromZero), (int)Math.Round((height - realFont.Metrics.Descent + 1f), MidpointRounding.AwayFromZero)); + } + + return Utils.Convert(size); + } + + public override void MeasureString(string str, RFont font, double maxWidth, out int charFit, out double charFitWidth) + { + // there is no need for it - used for text selection + throw new NotSupportedException(); + } + + public override void DrawString(string str, RFont font, RColor color, RPoint point, RSize size, bool rtl) + { + var skiaBrush = ((BrushAdapter)_adapter.GetSolidBrush(color)).Brush as SKPaint; + var skiaFont = ((FontAdapter)font).Font; + var skiaPoint = Utils.Convert(point); + //TODO: use the size. + var skiaSize = Utils.Convert(size); + + _canvas.DrawText(str, skiaPoint.X, skiaPoint.Y -skiaFont.Metrics.Ascent, skiaFont, skiaBrush); + } + + public override RBrush GetTextureBrush(RImage image, RRect dstRect, RPoint translateTransformLocation) + { + return new BrushAdapter(new TextureBrush(((ImageAdapter)image).Image, Utils.Convert(dstRect), Utils.Convert(translateTransformLocation))); + } + + public override RGraphicsPath GetGraphicsPath() + { + return new GraphicsPathAdapter(); + } + + public override void Dispose() + { + if (_releaseGraphics) + _canvas.Dispose(); + } + + + #region Delegate graphics methods + + public override void DrawLine(RPen pen, double x1, double y1, double x2, double y2) + { + _canvas.DrawLine((float)x1, (float)y1, (float)x2, (float)y2, ((PenAdapter)pen).Pen); + } + + public override void DrawRectangle(RPen pen, double x, double y, double width, double height) + { + _canvas.DrawRect((float)x, (float)y, (float)width, (float)height, ((PenAdapter)pen).Pen); + } + + public override void DrawRectangle(RBrush brush, double x, double y, double width, double height) + { + var xBrush = ((BrushAdapter)brush).Brush; + var xTextureBrush = xBrush as TextureBrush; + if (xTextureBrush != null) + { + xTextureBrush.DrawRectangle(_canvas, x, y, width, height); + } + else + { + _canvas.DrawRect((float)x, (float)y, (float)width, (float)height, (SKPaint)xBrush); + } + } + + public override void DrawImage(RImage image, RRect destRect, RRect srcRect) + { + var adapter = (ImageAdapter)image; + if (adapter.Picture != null) + { + //TODO: the resizing part.. + _canvas.DrawPicture(adapter.Picture, Utils.Convert(destRect).Location); + } + else + { + _canvas.DrawImage(adapter.Image, Utils.Convert(destRect), Utils.Convert(srcRect)); + } + //_canvas.DrawImage(((ImageAdapter)image).Image, Utils.Convert(destRect), Utils.Convert(srcRect), XGraphicsUnit.Point); + } + + public override void DrawImage(RImage image, RRect destRect) + { + var adapter = (ImageAdapter)image; + if (adapter.Picture != null) + { + //TODO: the resizing part.. + _canvas.DrawPicture(adapter.Picture, Utils.Convert(destRect).Location); + } + else + { + _canvas.DrawImage(((ImageAdapter)image).Image, Utils.Convert(destRect)); + } + + } + + public override void DrawPath(RPen pen, RGraphicsPath path) + { + _canvas.DrawPath(((GraphicsPathAdapter)path).GraphicsPath, ((PenAdapter)pen).Pen); + } + + public override void DrawPath(RBrush brush, RGraphicsPath path) + { + var paint = ((BrushAdapter)brush).Brush as SKPaint; + + if (paint == null) + { + //Could be a textured path. We don't support it right now :/ + return; + } + + _canvas.DrawPath(((GraphicsPathAdapter)path).GraphicsPath, paint); + } + + public override void DrawPolygon(RBrush brush, RPoint[] points) + { + var paint = ((BrushAdapter)brush).Brush as SKPaint; + + if (paint == null) + { + //Could be a textured path. We don't support it right now :/ + return; + } + + if (points != null && points.Length > 0) + { + var path = new SKPath(); + var skPoints = points.Select(n => Utils.Convert(n)).ToArray(); + path.AddPoly(skPoints); + _canvas.DrawPath(path, paint); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsPathAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsPathAdapter.cs new file mode 100644 index 000000000..2e1efb532 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/GraphicsPathAdapter.cs @@ -0,0 +1,101 @@ +// "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 SkiaSharp; +using System; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms graphics path object for core. + /// + internal sealed class GraphicsPathAdapter : RGraphicsPath + { + /// + /// The actual PdfSharp graphics path instance. + /// + private readonly SKPath _graphicsPath = new SKPath(); + + /// + /// The actual PdfSharp graphics path instance. + /// + public SKPath GraphicsPath + { + get { return _graphicsPath; } + } + + public override void Start(double x, double y) + { + _graphicsPath.MoveTo((float)x, (float)y); + } + + public override void LineTo(double x, double y) + { + _graphicsPath.LineTo((float)x, (float)y); + } + + public override void ArcTo(double x, double y, double size, Corner corner) + { + + switch (corner) + { + case Corner.TopLeft: + break; + case Corner.TopRight: + break; + case Corner.BottomLeft: + break; + case Corner.BottomRight: + break; + default: + break; + } + + float left = (float)(Math.Min(x, _graphicsPath.LastPoint.X) - (corner == Corner.TopRight || corner == Corner.BottomRight ? size : 0)); + float top = (float)(Math.Min(y, _graphicsPath.LastPoint.Y) - (corner == Corner.BottomLeft || corner == Corner.BottomRight ? size : 0)); + + _graphicsPath.AddArc(new SKRect(left, top, left + ((float)size * 2), top + ((float)size * 2)), GetStartAngle(corner), 90); + _graphicsPath.MoveTo((float)x, (float)y); + } + + public override void Dispose() + { } + + /// + /// Get arc start angle for the given corner. + /// + private static int GetStartAngle(Corner corner) + { + int startAngle; + switch (corner) + { + case Corner.TopLeft: + startAngle = 180; + break; + case Corner.TopRight: + startAngle = 270; + break; + case Corner.BottomLeft: + startAngle = 90; + break; + case Corner.BottomRight: + startAngle = 0; + break; + default: + throw new ArgumentOutOfRangeException("corner"); + } + return startAngle; + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/ImageAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/ImageAdapter.cs new file mode 100644 index 000000000..e49e949dd --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/ImageAdapter.cs @@ -0,0 +1,72 @@ +// "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 TheArtOfDev.HtmlRenderer.Adapters; +using SkiaSharp; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms Image object for core. + /// + internal sealed class ImageAdapter : RImage + { + /// + /// the underline win-forms image. + /// + private readonly SKImage _image; + private readonly SKPicture? _picture; + + public ImageAdapter(SKImage image) + { + _image = image; + _picture = null; + } + + /// + /// Initializes a new instance of the class. + /// + public ImageAdapter(SKImage image, SKPicture picture) + { + _image = image; + _picture = picture; + } + + /// + /// the underline win-forms image. + /// + public SKImage Image + { + get { return _image; } + } + + public SKPicture? Picture + { + get { return _picture; } + } + + public override double Width + { + get { return _image.Width; } + } + + public override double Height + { + get { return _image.Height; } + } + + public override void Dispose() + { + _image.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/PenAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/PenAdapter.cs new file mode 100644 index 000000000..0ca0a8594 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/PenAdapter.cs @@ -0,0 +1,94 @@ +// "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 TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using SkiaSharp; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for WinForms pens objects for core. + /// + internal sealed class PenAdapter : RPen + { + /// + /// The actual WinForms brush instance. + /// + private readonly SKPaint _pen; + + /// + /// Init. + /// + public PenAdapter(SKPaint pen) + { + _pen = pen; + } + + /// + /// The actual WinForms brush instance. + /// + public SKPaint Pen + { + get { return _pen; } + } + + public override double Width + { + get { return _pen.StrokeWidth; } + set { _pen.StrokeWidth = (float)value; } + } + + public override RDashStyle DashStyle + { + set + { + switch (value) + { + case RDashStyle.Solid: + _pen.PathEffect = null; + //_pen.DashStyle = XDashStyle.Solid; + break; + case RDashStyle.Dash: + _pen.PathEffect = SKPathEffect.CreateDash(new float[] { 10, 10 }, 0); + if (Width < 2) + { + _pen.PathEffect = SKPathEffect.CreateDash(new float[] { 4, 4 }, 0); + } + //_pen.DashStyle = XDashStyle.Dash; + //if (Width < 2) + // _pen.DashPattern = new[] { 4, 4d }; // better looking + break; + case RDashStyle.Dot: + _pen.PathEffect = SKPathEffect.CreateDash(new float[] { 2, 10 }, 5); + //_pen.DashStyle = XDashStyle.Dot; + break; + + //TODO: support these. + //case RDashStyle.DashDot: + // _pen.DashStyle = XDashStyle.DashDot; + // break; + //case RDashStyle.DashDotDot: + // _pen.DashStyle = XDashStyle.DashDotDot; + // break; + //case RDashStyle.Custom: + // _pen.DashStyle = XDashStyle.Custom; + // break; + default: + _pen.PathEffect = null; + //_pen.DashStyle = XDashStyle.Solid + break; + } + } + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/SkiaSharpAdapter.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/SkiaSharpAdapter.cs new file mode 100644 index 000000000..e545df777 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/SkiaSharpAdapter.cs @@ -0,0 +1,184 @@ +// "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 SkiaSharp; +using System.Drawing; +using System.Drawing.Text; +using System.IO; +using TheArtOfDev.HtmlRenderer.Adapters; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +using TheArtOfDev.HtmlRenderer.SkiaSharp.Utilities; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Adapter for SkiaSharp library platform. + /// + internal sealed class SkiaSharpAdapter : RAdapter + { + #region Fields and Consts + + /// + /// Singleton instance of global adapter. + /// + private static readonly SkiaSharpAdapter _instance = new SkiaSharpAdapter(); + + #endregion + + + /// + /// Init color resolve. + /// + private SkiaSharpAdapter() + { + AddFontFamilyMapping("monospace", "Courier New"); + AddFontFamilyMapping("Helvetica", "Arial"); + + /* var families = new InstalledFontCollection(); + + foreach (var family in families.Families) + { + AddFontFamily(new FontFamilyAdapter(new SKFontFamily(family.Name))); + }*/ + } + + /// + /// Singleton instance of global adapter. + /// + public static SkiaSharpAdapter Instance + { + get { return _instance; } + } + + protected override RColor GetColorInt(string colorName) + { + try + { + var color = Color.FromKnownColor((KnownColor)System.Enum.Parse(typeof(KnownColor), colorName, true)); + return Utils.Convert(color); + } + catch + { + return RColor.Empty; + } + } + + protected override RPen CreatePen(RColor color) + { + return new PenAdapter(new SKPaint { Color = Utils.Convert(color) }); + } + + protected override RBrush CreateSolidBrush(RColor color) + { + SKPaint solidBrush; + if (color == RColor.White) + solidBrush = new SKPaint { Color = SKColor.Parse("FFFFFFFF") };// XBrushes.White; + else if (color == RColor.Black) + solidBrush = new SKPaint { Color = SKColor.Parse("FF000000") };// XBrushes.White; // XBrushes.Black; + else if (color.A < 1) + solidBrush = new SKPaint { Color = SKColor.Parse("00000000") };// XBrushes.Transparent; + else + solidBrush = new SKPaint { Color = Utils.Convert(color) }; + + return new BrushAdapter(solidBrush); + } + + protected override RBrush CreateLinearGradientBrush(RRect rect, RColor color1, RColor color2, double angle) + { + //XLinearGradientMode mode; + //if (angle < 45) + // mode = XLinearGradientMode.ForwardDiagonal; + //else if (angle < 90) + // mode = XLinearGradientMode.Vertical; + //else if (angle < 135) + // mode = XLinearGradientMode.BackwardDiagonal; + //else + // mode = XLinearGradientMode.Horizontal; + //return new BrushAdapter(new XLinearGradientBrush(Utils.Convert(rect), Utils.Convert(color1), Utils.Convert(color2), mode)); + + var skRect = Utils.Convert(rect); + var start = skRect.Location; + var end = new SKPoint(skRect.Right, skRect.Bottom); + var paint = new SKPaint + { + Shader = SKShader.CreateLinearGradient(start, end, new[] { Utils.Convert(color1), Utils.Convert(color2) }, SKShaderTileMode.Clamp) + }; + + return new BrushAdapter(paint); + } + + protected override RImage ConvertImageInt(object image) + { + return image != null ? new ImageAdapter((SKImage)image) : null; + } + + protected override RImage ImageFromStreamInt(Stream memoryStream) + { + var image = SKImage.FromEncodedData(memoryStream); + if (image == null) + { + //Maybe an SVG? probably a better way than doing this, as we've basically failed to load into SKImage first. + //Perhaps the html renderer can be changed to pass in the content-type. TODO: wwmd? (what would mozilla do?) + memoryStream.Seek(0, SeekOrigin.Begin); + var skSvg = new global::SkiaSharp.Extended.Svg.SKSvg(); + var picture = skSvg.Load(memoryStream); + //using (var ms = new MemoryStream()) + //{ + // //skSvg.Picture.ToImage(ms, SKColors.Empty, SKEncodedImageFormat.Png, 100, 1f, 1f, SKColorType.Rgba8888, SKAlphaType.Unknown, SKColorSpace.CreateSrgb()); + + // ms.Seek(0, SeekOrigin.Begin); + // image = SKImage.FromEncodedData(ms); + //} + image = SKImage.FromBitmap(new SKBitmap((int)skSvg.CanvasSize.Width, (int)skSvg.CanvasSize.Height)); + return new ImageAdapter(image, picture); + } + else + { + return new ImageAdapter(image); + } + } + + protected override RFont CreateFontInt(string family, double size, RFontStyle style) + { + SKFontStyleWeight weight = SKFontStyleWeight.Normal; + SKFontStyleWidth width = SKFontStyleWidth.Normal; + SKFontStyleSlant slant = SKFontStyleSlant.Upright; + + if (style.HasFlag(RFontStyle.Bold)) + { + weight = SKFontStyleWeight.Bold; + } + if (style.HasFlag(RFontStyle.Italic)) + { + slant = SKFontStyleSlant.Italic; + } + if (style.HasFlag(RFontStyle.Strikeout)) + { + + } + if (style.HasFlag(RFontStyle.Underline)) + { + + } + + var typeFace = SKTypeface.FromFamilyName(family, weight, width, slant); + //convert from points to pixels. + var font = new SKFont(typeFace, (float)size); + return new FontAdapter(font); + } + + protected override RFont CreateFontInt(RFontFamily family, double size, RFontStyle style) + { + return CreateFontInt(family.Name, size, style); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Adapters/TextureBrush.cs b/Source/HtmlRenderer.SkiaSharp/Adapters/TextureBrush.cs new file mode 100644 index 000000000..68af7e9bf --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Adapters/TextureBrush.cs @@ -0,0 +1,77 @@ +// "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 SkiaSharp; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters +{ + /// + /// Because PdfSharp doesn't support texture brush we need to implement it ourselves. + /// + internal sealed class TextureBrush + { + #region Fields/Consts + + /// + /// The image to draw in the brush + /// + private readonly SKImage _image; + + /// + /// the + /// + private readonly SKRect _dstRect; + + /// + /// the transform the location of the image to handle center alignment + /// + private readonly SKPoint _translateTransformLocation; + + #endregion + + /// + /// Init. + /// + public TextureBrush(SKImage image, SKRect dstRect, SKPoint translateTransformLocation) + { + _image = image; + _dstRect = dstRect; + _translateTransformLocation = translateTransformLocation; + } + + /// + /// Draw the texture image in the given graphics at the given location. + /// + public void DrawRectangle(SKCanvas g, double x, double y, double width, double height) + { + var prevState = g.Save(); + g.ClipRect(new SKRect((float)x, (float)y, (float)(x + width), (float)(y + height))); + //g.IntersectClip(new SKRect(x, y, width, height)); + + double rx = _translateTransformLocation.X; + double w = _image.Width, h = _image.Height; + while (rx < x + width) + { + double ry = _translateTransformLocation.Y; + while (ry < y + height) + { + g.DrawImage(_image, new SKPoint((float)rx, (float)ry)); + //g.DrawImage(_image, rx, ry, w, h); + ry += h; + } + rx += w; + } + + g.Restore(); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/HtmlContainer.cs b/Source/HtmlRenderer.SkiaSharp/HtmlContainer.cs new file mode 100644 index 000000000..4d6878fa3 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/HtmlContainer.cs @@ -0,0 +1,356 @@ +// "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 SkiaSharp; +using System; +using System.Collections.Generic; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; +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 +{ + /// + /// Low level handling of Html Renderer logic, this class is used by . + /// + /// + public sealed class HtmlContainer : IDisposable + { + #region Fields and Consts + + /// + /// The internal core html container + /// + private readonly HtmlContainerInt _htmlContainerInt; + + #endregion + + + /// + /// Init. + /// + public HtmlContainer() + { + _htmlContainerInt = new HtmlContainerInt(SkiaSharpAdapter.Instance); + _htmlContainerInt.AvoidAsyncImagesLoading = true; + _htmlContainerInt.AvoidImagesLateLoading = true; + } + + /// + /// Raised when the set html document has been fully loaded.
+ /// Allows manipulation of the html dom, scroll position, etc. + ///
+ public event EventHandler LoadComplete + { + add { _htmlContainerInt.LoadComplete += value; } + remove { _htmlContainerInt.LoadComplete -= value; } + } + + /// + /// Raised when an error occurred during html rendering.
+ ///
+ /// + /// There is no guarantee that the event will be raised on the main thread, it can be raised on thread-pool thread. + /// + public event EventHandler RenderError + { + add { _htmlContainerInt.RenderError += value; } + remove { _htmlContainerInt.RenderError -= value; } + } + + /// + /// Raised when a stylesheet is about to be loaded by file path or URI by link element.
+ /// This event allows to provide the stylesheet manually or provide new source (file or Uri) to load from.
+ /// If no alternative data is provided the original source will be used.
+ ///
+ public event EventHandler StylesheetLoad + { + add { _htmlContainerInt.StylesheetLoad += value; } + remove { _htmlContainerInt.StylesheetLoad -= value; } + } + + /// + /// Raised when an image is about to be loaded by file path or URI.
+ /// This event allows to provide the image manually, if not handled the image will be loaded from file or download from URI. + ///
+ public event EventHandler ImageLoad + { + add { _htmlContainerInt.ImageLoad += value; } + remove { _htmlContainerInt.ImageLoad -= value; } + } + + /// + /// The internal core html container + /// + internal HtmlContainerInt HtmlContainerInt + { + get { return _htmlContainerInt; } + } + + /// + /// the parsed stylesheet data used for handling the html + /// + public CssData CssData + { + get { return _htmlContainerInt.CssData; } + } + + /// + /// Gets or sets a value indicating if anti-aliasing should be avoided for geometry like backgrounds and borders (default - false). + /// + public bool AvoidGeometryAntialias + { + get { return _htmlContainerInt.AvoidGeometryAntialias; } + set { _htmlContainerInt.AvoidGeometryAntialias = value; } + } + + /// + /// The scroll offset of the html.
+ /// This will adjust the rendered html by the given offset so the content will be "scrolled".
+ ///
+ /// + /// Element that is rendered at location (50,100) with offset of (0,200) will not be rendered as it + /// will be at -100 therefore outside the client rectangle. + /// + public SKPoint ScrollOffset + { + get { return Utils.Convert(_htmlContainerInt.ScrollOffset); } + set { _htmlContainerInt.ScrollOffset = Utils.Convert(value); } + } + + /// + /// The top-left most location of the rendered html.
+ /// This will offset the top-left corner of the rendered html. + ///
+ public SKPoint Location + { + get { return Utils.Convert(_htmlContainerInt.Location); } + set { _htmlContainerInt.Location = Utils.Convert(value); } + } + + /// + /// The max width and height of the rendered html.
+ /// The max width will effect the html layout wrapping lines, resize images and tables where possible.
+ /// The max height does NOT effect layout, but will not render outside it (clip).
+ /// can be exceed the max size by layout restrictions (unwrappable line, set image size, etc.).
+ /// Set zero for unlimited (width\height separately).
+ ///
+ public SKSize MaxSize + { + get { return Utils.Convert(_htmlContainerInt.MaxSize); } + set { _htmlContainerInt.MaxSize = Utils.Convert(value); } + } + + /// + /// The actual size of the rendered html (after layout) + /// + public SKSize ActualSize + { + get { return Utils.Convert(_htmlContainerInt.ActualSize); } + internal set { _htmlContainerInt.ActualSize = Utils.Convert(value); } + } + + public SKSize PageSize { + get + { + return new SKSize((float)_htmlContainerInt.PageSize.Width, (float)_htmlContainerInt.PageSize.Height); + } + set + { + _htmlContainerInt.PageSize = new RSize(value.Width, value.Height); + } + } + + /// + /// the top margin between the page start and the text + /// + public int MarginTop + { + get { return _htmlContainerInt.MarginTop; } + set + { + if (value > -1) + _htmlContainerInt.MarginTop = value; + } + } + + /// + /// the bottom margin between the page end and the text + /// + public int MarginBottom + { + get { return _htmlContainerInt.MarginBottom; } + set + { + if (value > -1) + _htmlContainerInt.MarginBottom = value; + } + } + + /// + /// the left margin between the page start and the text + /// + public int MarginLeft + { + get { return _htmlContainerInt.MarginLeft; } + set + { + if (value > -1) + _htmlContainerInt.MarginLeft = value; + } + } + + /// + /// the right margin between the page end and the text + /// + public int MarginRight + { + get { return _htmlContainerInt.MarginRight; } + set + { + if (value > -1) + _htmlContainerInt.MarginRight = value; + } + } + + /// + /// Set all 4 margins to the given value. + /// + /// + public void SetMargins(int value) + { + if (value > -1) + _htmlContainerInt.SetMargins(value); + } + + /// + /// Get the currently selected text segment in the html. + /// + public string SelectedText + { + get { return _htmlContainerInt.SelectedText; } + } + + /// + /// Copy the currently selected html segment with style. + /// + public string SelectedHtml + { + get { return _htmlContainerInt.SelectedHtml; } + } + + /// + /// Init with optional document and stylesheet. + /// + /// the html to init with, init empty if not given + /// optional: the stylesheet to init with, init default if not given + public void SetHtml(string htmlSource, CssData baseCssData = null) + { + _htmlContainerInt.SetHtml(htmlSource, baseCssData); + } + + /// + /// Get html from the current DOM tree with style if requested. + /// + /// Optional: controls the way styles are generated when html is generated (default: ) + /// generated html + public string GetHtml(HtmlGenerationStyle styleGen = HtmlGenerationStyle.Inline) + { + return _htmlContainerInt.GetHtml(styleGen); + } + + /// + /// Get attribute value of element at the given x,y location by given key.
+ /// If more than one element exist with the attribute at the location the inner most is returned. + ///
+ /// the location to find the attribute at + /// the attribute key to get value by + /// found attribute value or null if not found + public string GetAttributeAt(SKPoint location, string attribute) + { + return _htmlContainerInt.GetAttributeAt(Utils.Convert(location), attribute); + } + + /// + /// Get all the links in the HTML with the element rectangle and href data. + /// + /// collection of all the links in the HTML + public List> GetLinks() + { + var linkElements = new List>(); + foreach (var link in HtmlContainerInt.GetLinks()) + { + linkElements.Add(new LinkElementData(link.Id, link.Href, Utils.Convert(link.Rectangle))); + } + return linkElements; + } + + /// + /// Get css link href at the given x,y location. + /// + /// the location to find the link at + /// css link href if exists or null + public string GetLinkAt(SKPoint location) + { + return _htmlContainerInt.GetLinkAt(Utils.Convert(location)); + } + + /// + /// Get the rectangle of html element as calculated by html layout.
+ /// Element if found by id (id attribute on the html element).
+ /// Note: to get the screen rectangle you need to adjust by the hosting control.
+ ///
+ /// the id of the element to get its rectangle + /// the rectangle of the element or null if not found + public SKRect? GetElementRectangle(string elementId) + { + var r = _htmlContainerInt.GetElementRectangle(elementId); + return r.HasValue ? Utils.Convert(r.Value) : (SKRect?)null; + } + + /// + /// Measures the bounds of box and children, recursively. + /// + /// Device context to draw + public void PerformLayout(SKCanvas g, SKBitmap b) + { + ArgChecker.AssertArgNotNull(g, "g"); + + using (var ig = new GraphicsAdapter(g, b)) + { + _htmlContainerInt.PerformLayout(ig); + } + } + + /// + /// Render the html using the given device. + /// + /// the device to use to render + public void PerformPaint(SKCanvas g, SKBitmap b) + { + ArgChecker.AssertArgNotNull(g, "g"); + + using (var ig = new GraphicsAdapter(g, b)) + { + _htmlContainerInt.PerformPaint(ig); + } + } + + public void Dispose() + { + _htmlContainerInt.Dispose(); + } + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/HtmlRenderer.SkiaSharp.csproj b/Source/HtmlRenderer.SkiaSharp/HtmlRenderer.SkiaSharp.csproj new file mode 100644 index 000000000..d2c9db518 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/HtmlRenderer.SkiaSharp.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Source/HtmlRenderer.SkiaSharp/PdfGenerateConfig.cs b/Source/HtmlRenderer.SkiaSharp/PdfGenerateConfig.cs new file mode 100644 index 000000000..f221c78a7 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/PdfGenerateConfig.cs @@ -0,0 +1,93 @@ +// "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 SkiaSharp; +using System.Drawing; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp +{ + public enum PageSizeType + { + A4, + Letter + } + + /// + /// The settings for generating PDF using + /// + public sealed class PdfGenerateConfig + { + /// + /// the page size to use for each page in the generated pdf + /// + public PageSizeType? PageSize { get; set; } + + /// + /// if the page size is undefined this allow you to set manually the page size + /// + public SizeF ManualPageSize { get; set; } + + /// + /// TODO: use an enum for portrait/landscape. + /// + public bool IsLandscape { get; set; } = false; + + /// + /// the top margin in pixels between the page start and the text. + /// + public int MarginTop { get; set; } + + /// + /// the bottom margin in pixels between the page end and the text + /// + public int MarginBottom { get; set; } + + /// + /// the left margin in pixels between the page start and the text + /// + public int MarginLeft { get; set; } + + /// + /// the right margin in pixels between the page end and the text + /// + public int MarginRight { get; set; } + + /// + /// Set all 4 margins to the given value. + /// + /// + public void SetMargins(int value) + { + if (value > -1) + MarginBottom = MarginLeft = MarginTop = MarginRight = value; + } + + // The international definitions are: + // 1 inch == 25.4 mm + // 1 inch == 72 point + + /// + /// Convert the units passed in milimiters to the units used in PdfSharp + /// + /// + /// + /// + public static SKSize MilimitersToPixels(float width, float height) + { + return new SKSize(width * 3.779527559f, height * 3.779527559f); + } + + //TODO: single util. + public static int MilimitersToPixels(float mm) + => (int)(mm * 3.779527559f * 72 / 96); + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/PdfGenerator.cs b/Source/HtmlRenderer.SkiaSharp/PdfGenerator.cs new file mode 100644 index 000000000..e983af363 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/PdfGenerator.cs @@ -0,0 +1,254 @@ +// "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 SkiaSharp; +using System; +using System.Drawing; +using TheArtOfDev.HtmlRenderer.Core; +using TheArtOfDev.HtmlRenderer.Core.Entities; +using TheArtOfDev.HtmlRenderer.Core.Utils; +using TheArtOfDev.HtmlRenderer.SkiaSharp.Adapters; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp +{ + /// + /// TODO:a add doc + /// + public static class PdfGenerator + { + /// + /// 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 PDF document from given HTML.
+ ///
+ /// HTML source to create PDF from + /// the page size to use for each page in the generated pdf + /// the margin to use between the HTML and the edges of each page + /// 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 Stream GeneratePdf(string html, PageSizeType pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var config = new PdfGenerateConfig(); + config.PageSize = pageSize; + config.SetMargins(margin); + return GeneratePdf(html, config, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Create PDF document from given HTML.
+ ///
+ /// HTML source to create PDF from + /// the configuration to use for the PDF generation (page size/page orientation/margins/etc.) + /// 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 Stream GeneratePdf(string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var ms = new MemoryStream(); + var pdfDoc = SKDocument.CreatePdf(ms, 96); + + // add rendered PDF pages to document + AddPdfPages(pdfDoc, html, config, cssData, stylesheetLoad, imageLoad); + + pdfDoc.Close(); + ms.Seek(0, SeekOrigin.Begin); + return ms; + } + + /// + /// Create PDF pages from given HTML and appends them to the provided PDF document.
+ ///
+ /// PDF document to append pages to + /// HTML source to create PDF from + /// the page size to use for each page in the generated pdf + /// the margin to use between the HTML and the edges of each page + /// 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 void AddPdfPages(SKDocument document, string html, PageSizeType pageSize, int margin = 20, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + var config = new PdfGenerateConfig(); + config.PageSize = pageSize; + config.SetMargins(margin); + AddPdfPages(document, html, config, cssData, stylesheetLoad, imageLoad); + } + + /// + /// Create PDF pages from given HTML and appends them to the provided PDF document.
+ ///
+ /// PDF document to append pages to + /// HTML source to create PDF from + /// the configuration to use for the PDF generation (page size/page orientation/margins/etc.) + /// 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 void AddPdfPages(SKDocument document, string html, PdfGenerateConfig config, CssData cssData = null, EventHandler stylesheetLoad = null, EventHandler imageLoad = null) + { + SKSize orgPageSize; + // get the size of each page to layout the HTML in + if (config.PageSize != null) + orgPageSize = ConvertToSize(config.PageSize.Value); + else + orgPageSize = new SKSize(config.ManualPageSize.Width, config.ManualPageSize.Height); + + //if (config.PageOrientation == PageOrientation.Landscape) + if (config.IsLandscape) + { + // invert pagesize for landscape + orgPageSize = new SKSize(orgPageSize.Height, orgPageSize.Width); + } + + var pageSize = new SKSize(orgPageSize.Width - config.MarginLeft - config.MarginRight, orgPageSize.Height - config.MarginTop - config.MarginBottom); + + if (!string.IsNullOrEmpty(html)) + { + var bitmap = new SKBitmap((int)pageSize.Width, (int)(pageSize.Height)); + + using (var container = new HtmlContainer()) + { + if (stylesheetLoad != null) + container.StylesheetLoad += stylesheetLoad; + if (imageLoad != null) + container.ImageLoad += imageLoad; + + container.Location = new SKPoint(config.MarginLeft, config.MarginTop); + container.MaxSize = new SKSize(pageSize.Width, 0); + container.SetHtml(html, cssData); + container.PageSize = pageSize; + container.MarginBottom = config.MarginBottom; + container.MarginLeft = config.MarginLeft; + container.MarginRight = config.MarginRight; + container.MarginTop = config.MarginTop; + + //Measure the content. + var canvas = new SKCanvas(bitmap); + container.PerformLayout(canvas, bitmap); + + float scrollOffset = 0; + while (scrollOffset > -container.ActualSize.Height) + { + canvas = document.BeginPage(orgPageSize.Width, orgPageSize.Height); + + canvas.ClipRect(new SKRect(config.MarginLeft, config.MarginTop, config.MarginLeft + pageSize.Width, config.MarginTop + pageSize.Height)); + + container.ScrollOffset = new SKPoint(0, scrollOffset); + container.PerformPaint(canvas, bitmap); + + canvas.Flush(); + document.EndPage(); + scrollOffset -= pageSize.Height; + } + + //// add web links and anchors + //HandleLinks(document, container, orgPageSize, pageSize); + } + } + } + + private static SKSize ConvertToSize(PageSizeType standardPageSize) + { + SKSize mmSize; + switch (standardPageSize) + { + case PageSizeType.A4: + mmSize = new SKSize(PdfGenerateConfig.MilimitersToPixels(210), PdfGenerateConfig.MilimitersToPixels(297)); + break; + + case PageSizeType.Letter: + mmSize = new SKSize(PdfGenerateConfig.MilimitersToPixels(279.4f), PdfGenerateConfig.MilimitersToPixels(215.9f)); + break; + + default: + throw new NotSupportedException($"Unhandled page size {standardPageSize}"); + } + + return mmSize; + } + + + + #region Private/Protected methods + + ///// + ///// Handle HTML links by create PDF Documents link either to external URL or to another page in the document. + ///// + //private static void HandleLinks(PdfDocument document, HtmlContainer container, SKSize orgPageSize, SKSize pageSize) + //{ + // foreach (var link in container.GetLinks()) + // { + // int i = (int)(link.Rectangle.Top / pageSize.Height); + // for (; i < document.Pages.Count && pageSize.Height * i < link.Rectangle.Bottom; i++) + // { + // var offset = pageSize.Height * i; + + // // fucking position is from the bottom of the page + // var xRect = new SKRect(link.Rectangle.Left, orgPageSize.Height - (link.Rectangle.Height + link.Rectangle.Top - offset), link.Rectangle.Width, link.Rectangle.Height); + + // if (link.IsAnchor) + // { + // // create link to another page in the document + // var anchorRect = container.GetElementRectangle(link.AnchorId); + // if (anchorRect.HasValue) + // { + // // document links to the same page as the link is not allowed + // int anchorPageIdx = (int)(anchorRect.Value.Top / pageSize.Height); + // if (i != anchorPageIdx) + // document.Pages[i].AddDocumentLink(new PdfRectangle(xRect), anchorPageIdx); + // } + // } + // else + // { + // // create link to URL + // document.Pages[i].AddWebLink(new PdfRectangle(xRect), link.Href); + // } + // } + // } + //} + + #endregion + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.SkiaSharp/Utilities/Utils.cs b/Source/HtmlRenderer.SkiaSharp/Utilities/Utils.cs new file mode 100644 index 000000000..9af233a05 --- /dev/null +++ b/Source/HtmlRenderer.SkiaSharp/Utilities/Utils.cs @@ -0,0 +1,100 @@ +// "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 SkiaSharp; +using System.Drawing; +using TheArtOfDev.HtmlRenderer.Adapters.Entities; + +namespace TheArtOfDev.HtmlRenderer.SkiaSharp.Utilities +{ + /// + /// Utilities for converting WinForms entities to HtmlRenderer core entities. + /// + internal static class Utils + { + /// + /// Convert from WinForms point to core point. + /// + public static RPoint Convert(SKPoint p) + { + return new RPoint(p.X, p.Y); + } + + /// + /// Convert from WinForms point to core point. + /// + public static SKPoint[] Convert(RPoint[] points) + { + SKPoint[] myPoints = new SKPoint[points.Length]; + for (int i = 0; i < points.Length; i++) + myPoints[i] = Convert(points[i]); + return myPoints; + } + + /// + /// Convert from core point to WinForms point. + /// + public static SKPoint Convert(RPoint p) + { + return new SKPoint((float)p.X, (float)p.Y); + } + + /// + /// Convert from WinForms size to core size. + /// + public static RSize Convert(SKSize s) + { + return new RSize(s.Width, s.Height); + } + + /// + /// Convert from core size to WinForms size. + /// + public static SKSize Convert(RSize s) + { + return new SKSize((float)s.Width, (float)s.Height); + } + + /// + /// Convert from WinForms rectangle to core rectangle. + /// + public static RRect Convert(SKRect r) + { + return new RRect(r.Left, r.Top, r.Width, r.Height); + } + + /// + /// Convert from core rectangle to WinForms rectangle. + /// + public static SKRect Convert(RRect r) + { + return new SKRect((float)r.X, (float)r.Y, (float)(r.X + r.Width), (float)(r.Y + r.Height)); + } + + /// + /// Convert from core color to WinForms color. + /// + public static SKColor Convert(RColor c) + { + return new SKColor(c.R, c.G, c.B, c.A); + } + + /// + /// Convert from color to WinForms color. + /// + public static RColor Convert(Color c) + { + return RColor.FromArgb(c.A, c.R, c.G, c.B); + } + + } +} \ No newline at end of file diff --git a/Source/HtmlRenderer.sln b/Source/HtmlRenderer.sln index 21c55cd32..4476a9f6c 100644 --- a/Source/HtmlRenderer.sln +++ b/Source/HtmlRenderer.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29613.14 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34227.203 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{E263EA16-2E6A-4269-A319-AA2F97ADA8E1}" EndProject @@ -30,6 +30,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlRenderer.Core", "HtmlRe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlRenderer.PdfSharp.Core", "HtmlRenderer.PdfSharp.Core\HtmlRenderer.PdfSharp.Core.csproj", "{D7BF9146-867B-472F-BB83-F72EA56E3945}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HtmlRenderer.Demo.Console", "Demo\Console\HtmlRenderer.Demo.Console\HtmlRenderer.Demo.Console.csproj", "{599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.SkiaSharp", "HtmlRenderer.SkiaSharp\HtmlRenderer.SkiaSharp.csproj", "{3E96A32C-197C-4823-9C04-BF637DB970E1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +138,30 @@ Global {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|Mixed Platforms.Build.0 = Release|Any CPU {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|x86.ActiveCfg = Release|Any CPU {D7BF9146-867B-472F-BB83-F72EA56E3945}.Release|x86.Build.0 = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|x86.ActiveCfg = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Debug|x86.Build.0 = Debug|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|Any CPU.Build.0 = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|x86.ActiveCfg = Release|Any CPU + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9}.Release|x86.Build.0 = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Debug|x86.Build.0 = Debug|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|Any CPU.Build.0 = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|x86.ActiveCfg = Release|Any CPU + {3E96A32C-197C-4823-9C04-BF637DB970E1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -142,6 +170,7 @@ Global {2390B71F-9400-47F4-B23A-7F2649C87D35} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} {8AD34FE8-8382-4A8A-B3AA-A0392ED42423} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} {F02E0216-4AE3-474F-9381-FCB93411CDB0} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} + {599EC554-42E0-4CBD-8C1E-BF3E8CECB5A9} = {E263EA16-2E6A-4269-A319-AA2F97ADA8E1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2169EDEB-46C2-46B7-AB8E-C3ECC7B5568D}