From 435c42ab9e2cb9205aecaf3699e5605ec37f690d Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Wed, 30 Mar 2022 23:37:37 +0200 Subject: [PATCH] Changed previewer communication layer to http --- QuestPDF/Assembly.cs | 7 - QuestPDF/Drawing/DocumentGenerator.cs | 7 + QuestPDF/Drawing/SkiaPictureCanvas.cs | 55 ++++++++ QuestPDF/Previewer/ExceptionDocument.cs | 108 ++++++++++++++ QuestPDF/Previewer/HotReloadManager.cs | 23 +++ QuestPDF/Previewer/PreviewerExtensions.cs | 46 ++++++ QuestPDF/Previewer/PreviewerRefreshCommand.cs | 28 ++++ QuestPDF/Previewer/PreviewerService.cs | 132 ++++++++++++++++++ QuestPDF/QuestPDF.csproj | 12 +- QuestPDF/Resources/ReleaseNotes.txt | 13 +- 10 files changed, 412 insertions(+), 19 deletions(-) delete mode 100644 QuestPDF/Assembly.cs create mode 100644 QuestPDF/Drawing/SkiaPictureCanvas.cs create mode 100644 QuestPDF/Previewer/ExceptionDocument.cs create mode 100644 QuestPDF/Previewer/HotReloadManager.cs create mode 100644 QuestPDF/Previewer/PreviewerExtensions.cs create mode 100644 QuestPDF/Previewer/PreviewerRefreshCommand.cs create mode 100644 QuestPDF/Previewer/PreviewerService.cs diff --git a/QuestPDF/Assembly.cs b/QuestPDF/Assembly.cs deleted file mode 100644 index 6d8b210..0000000 --- a/QuestPDF/Assembly.cs +++ /dev/null @@ -1,7 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -[assembly: InternalsVisibleTo("QuestPDF.Previewer")] -[assembly: InternalsVisibleTo("QuestPDF.UnitTests")] -[assembly: InternalsVisibleTo("QuestPDF.Examples")] -[assembly: InternalsVisibleTo("QuestPDF.ReportSample")] \ No newline at end of file diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 2a5882c..6e43689 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -51,6 +51,13 @@ namespace QuestPDF.Drawing return canvas.Images; } + internal static ICollection GeneratePreviewerPictures(IDocument document) + { + var canvas = new SkiaPictureCanvas(); + RenderDocument(canvas, document); + return canvas.Pictures; + } + internal static void RenderDocument(TCanvas canvas, IDocument document) where TCanvas : ICanvas, IRenderingCanvas { diff --git a/QuestPDF/Drawing/SkiaPictureCanvas.cs b/QuestPDF/Drawing/SkiaPictureCanvas.cs new file mode 100644 index 0000000..fd5e1b6 --- /dev/null +++ b/QuestPDF/Drawing/SkiaPictureCanvas.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using SkiaSharp; + +namespace QuestPDF.Drawing +{ + public class PreviewerPicture + { + public SKPicture Picture { get; set; } + public Size Size { get; set; } + + public PreviewerPicture(SKPicture picture, Size size) + { + Picture = picture; + Size = size; + } + } + + internal class SkiaPictureCanvas : SkiaCanvasBase + { + private SKPictureRecorder? PictureRecorder { get; set; } + private Size? CurrentPageSize { get; set; } + + public ICollection Pictures { get; } = new List(); + + public override void BeginDocument() + { + Pictures.Clear(); + } + + public override void BeginPage(Size size) + { + CurrentPageSize = size; + PictureRecorder = new SKPictureRecorder(); + + Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height)); + } + + public override void EndPage() + { + var picture = PictureRecorder?.EndRecording(); + + if (picture != null && CurrentPageSize.HasValue) + Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value)); + + PictureRecorder?.Dispose(); + PictureRecorder = null; + } + + public override void EndDocument() { } + } +} \ No newline at end of file diff --git a/QuestPDF/Previewer/ExceptionDocument.cs b/QuestPDF/Previewer/ExceptionDocument.cs new file mode 100644 index 0000000..266a02b --- /dev/null +++ b/QuestPDF/Previewer/ExceptionDocument.cs @@ -0,0 +1,108 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; +using SkiaSharp; + +namespace QuestPDF.Previewer +{ + public class ExceptionDocument : IDocument + { + private Exception Exception { get; } + + public ExceptionDocument(Exception exception) + { + Exception = exception; + } + + public DocumentMetadata GetMetadata() + { + return DocumentMetadata.Default; + } + + public void Compose(IDocumentContainer document) + { + document.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(50); + page.DefaultTextStyle(x => x.FontSize(16)); + + page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium); + + page.Header() + .ShowOnce() + .PaddingBottom(5) + .Row(row => + { + row.Spacing(15); + + row.AutoItem() + .PaddingTop(15) + .Width(48) + .AspectRatio(1) + .Canvas((canvas, size) => + { + const float iconSize = 24; + using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7"); + + using var paint = new SKPaint() + { + Color = SKColors.Red, + IsAntialias = true + }; + + canvas.Scale(size.Width / iconSize); + canvas.DrawPath(iconPath, paint); + }); + + row.RelativeItem() + .Column(column => + { + column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold(); + column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold(); + }); + }); + + page.Content().PaddingVertical(20).Column(column => + { + var currentException = Exception; + + while (currentException != null) + { + column.Item() + .PaddingTop(25) + .PaddingBottom(15) + + .Padding(-10) + .Background(Colors.Grey.Lighten3) + .Padding(10) + + .Text(text => + { + text.DefaultTextStyle(x => x.FontSize(18)); + + text.Span(currentException.GetType().Name + ": ").Bold(); + text.Span(currentException.Message); + }); + + foreach (var trace in currentException.StackTrace.Split('\n')) + { + column + .Item() + .ShowEntire() + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .PaddingVertical(5) + .Text(trace.Trim()) + .FontSize(12); + } + + currentException = currentException.InnerException; + } + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Previewer/HotReloadManager.cs b/QuestPDF/Previewer/HotReloadManager.cs new file mode 100644 index 0000000..8b18c35 --- /dev/null +++ b/QuestPDF/Previewer/HotReloadManager.cs @@ -0,0 +1,23 @@ +#if NET6_0_OR_GREATER + +using System; + +[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))] + +namespace QuestPDF.Previewer +{ + /// + /// Helper for subscribing to hot reload notifications. + /// + internal static class HotReloadManager + { + public static event EventHandler? UpdateApplicationRequested; + + public static void UpdateApplication(Type[]? _) + { + UpdateApplicationRequested?.Invoke(null, EventArgs.Empty); + } + } +} + +#endif \ No newline at end of file diff --git a/QuestPDF/Previewer/PreviewerExtensions.cs b/QuestPDF/Previewer/PreviewerExtensions.cs new file mode 100644 index 0000000..c9684e5 --- /dev/null +++ b/QuestPDF/Previewer/PreviewerExtensions.cs @@ -0,0 +1,46 @@ +#if NET6_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using QuestPDF.Drawing; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Previewer +{ + public static class Extensions + { + public static async Task ShowInPreviewer(this IDocument document, int port = 12500) + { + var previewerService = new PreviewerService(port); + await previewerService.Connect(); + await RefreshPreview(); + + HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview(); + + while (true) + await Task.Delay(TimeSpan.FromSeconds(1)); + + Task RefreshPreview() + { + var pictures = GetPictures(); + return previewerService.RefreshPreview(pictures); + + ICollection GetPictures() + { + try + { + return DocumentGenerator.GeneratePreviewerPictures(document); + } + catch (Exception exception) + { + var exceptionDocument = new ExceptionDocument(exception); + return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument); + } + } + } + } + } +} + +#endif diff --git a/QuestPDF/Previewer/PreviewerRefreshCommand.cs b/QuestPDF/Previewer/PreviewerRefreshCommand.cs new file mode 100644 index 0000000..4e17ca8 --- /dev/null +++ b/QuestPDF/Previewer/PreviewerRefreshCommand.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using QuestPDF.Infrastructure; + +#if NET6_0_OR_GREATER + +namespace QuestPDF.Previewer +{ + internal class PreviewerRefreshCommand + { + public ICollection Pages { get; set; } + + public class Page + { + public string Id { get; } + + public float Width { get; init; } + public float Height { get; init; } + + public Page() + { + Id = Guid.NewGuid().ToString("N"); + } + } + } +} + +#endif \ No newline at end of file diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs new file mode 100644 index 0000000..e2abe06 --- /dev/null +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -0,0 +1,132 @@ +#if NET6_0_OR_GREATER + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using QuestPDF.Drawing; + +namespace QuestPDF.Previewer +{ + internal class PreviewerService + { + private HttpClient HttpClient { get; init; } + + public PreviewerService(int port) + { + HttpClient = new() + { + BaseAddress = new Uri($"http://localhost:{port}/"), + Timeout = TimeSpan.FromMilliseconds(100) + }; + } + + public async Task Connect() + { + var isConnected = await TestConnection(); + + if (isConnected) + return; + + StartPreviewer(); + await WaitForConnection(); + } + + private async Task TestConnection() + { + try + { + var result = await HttpClient.GetAsync("/ping"); + return result.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + private async Task GetPreviewerVersion() + { + var result = await HttpClient.GetAsync("/version"); + var value = await result.Content.ReadAsStringAsync(); + return Version.Parse(value); + } + + private static void StartPreviewer() + { + try + { + var process = new Process + { + StartInfo = new() + { + UseShellExecute = false, + FileName = "questpdf-test", + CreateNoWindow = true + } + }; + + process.Start(); + } + catch + { + throw new Exception("Cannot start the QuestPDF Previewer tool. Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'."); + } + } + + private void CheckVersionCompatibility(Version version) + { + + } + + private async Task WaitForConnection() + { + while (true) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + + var isConnected = await TestConnection(); + + if (isConnected) + break; + } + } + + public async Task RefreshPreview(ICollection pictures) + { + var multipartContent = new MultipartFormDataContent(); + + var pages = new List(); + + foreach (var picture in pictures) + { + var page = new PreviewerRefreshCommand.Page + { + Width = picture.Size.Width, + Height = picture.Size.Height + }; + + pages.Add(page); + + var pictureStream = picture.Picture.Serialize().AsStream(); + multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id); + } + + var command = new PreviewerRefreshCommand + { + Pages = pages + }; + + multipartContent.Add(JsonContent.Create(command), "command"); + + await HttpClient.PostAsync("/update/preview", multipartContent); + + foreach (var picture in pictures) + picture.Picture.Dispose(); + } + } +} + +#endif diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 9fb4b1f..b341213 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF - 2022.3.1 + 2022.4.0-alpha1 QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 9 @@ -24,7 +24,15 @@ - + + + + + + + + + diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index 06c3882..1b429e8 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,10 +1,3 @@ -- Added minimal API to help you quickly start with QuestPDF development and speed up prototyping, -- Improved exception message when desired font type cannot be found (instead of loading default font on Windows and failing with wrong characters on Linux), -- Improved support for custom font types: loading all type faces from a file, respecting true font family, using CSS-like algorithm to find best style match, -- Added support for custom page number formats in the `Text` element, e.g. you can implement roman literal style if required, -- Improved text API to make it more concise, -- Extended support for the `Section` element (previously the `Location` element) by tracking: beginning page number, end page number, page length, page number within location, -- Renaming: the `ExternalLink` element was renamed to the `Hyperlink` element, -- Renaming: the `Location` element was renamed to the `Section` element, -- Renaming: the `InternalLink` element was renamed to the `SectionLink` element, -- Updated homepage and GitHub pages content. +- Introduced the Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates the preview every time you make a code change (available only for dotnet 6), +- Improved default word-wrapping algorithm to better handle words which do not fit on the available width, +- Introduced new word-wrapping option 'WrapAnywhere' that wraps word at the last possible character instead of moving it into new line.