Selaa lähdekoodia

Changed previewer communication layer to http

MarcinZiabek 3 vuotta sitten
vanhempi
sitoutus
435c42ab9e

+ 0 - 7
QuestPDF/Assembly.cs

@@ -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")]

+ 7 - 0
QuestPDF/Drawing/DocumentGenerator.cs

@@ -51,6 +51,13 @@ namespace QuestPDF.Drawing
             return canvas.Images;
             return canvas.Images;
         }
         }
 
 
+        internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
+        {
+            var canvas = new SkiaPictureCanvas();
+            RenderDocument(canvas, document);
+            return canvas.Pictures;
+        }
+        
         internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
         internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
         {
         {

+ 55 - 0
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<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
+        
+        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() { }
+    }
+}

+ 108 - 0
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;
+                    }
+                });
+            });
+        }
+    }   
+}

+ 23 - 0
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
+{
+    /// <summary>
+    /// Helper for subscribing to hot reload notifications.
+    /// </summary>
+    internal static class HotReloadManager
+    {
+        public static event EventHandler? UpdateApplicationRequested;
+
+        public static void UpdateApplication(Type[]? _)
+        {
+            UpdateApplicationRequested?.Invoke(null, EventArgs.Empty);
+        }
+    }
+}
+
+#endif

+ 46 - 0
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<PreviewerPicture> GetPictures()
+                {
+                    try
+                    {
+                        return DocumentGenerator.GeneratePreviewerPictures(document);
+                    }
+                    catch (Exception exception)
+                    {
+                        var exceptionDocument = new ExceptionDocument(exception);
+                        return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
+                    }
+                }
+            }
+        }
+    }
+}
+
+#endif

+ 28 - 0
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<Page> 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

+ 132 - 0
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<bool> TestConnection()
+        {
+            try
+            {
+                var result = await HttpClient.GetAsync("/ping");
+                return result.IsSuccessStatusCode;
+            }
+            catch
+            {
+                return false;
+            }
+        }
+        
+        private async Task<Version> 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<PreviewerPicture> pictures)
+        {
+            var multipartContent = new MultipartFormDataContent();
+
+            var pages = new List<PreviewerRefreshCommand.Page>();
+            
+            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

+ 10 - 2
QuestPDF/QuestPDF.csproj

@@ -4,7 +4,7 @@
         <Authors>MarcinZiabek</Authors>
         <Authors>MarcinZiabek</Authors>
         <Company>CodeFlint</Company>
         <Company>CodeFlint</Company>
         <PackageId>QuestPDF</PackageId>
         <PackageId>QuestPDF</PackageId>
-        <Version>2022.3.1</Version>
+        <Version>2022.4.0-alpha1</Version>
         <PackageDescription>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.</PackageDescription>
         <PackageDescription>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.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
         <LangVersion>9</LangVersion>
         <LangVersion>9</LangVersion>
@@ -24,7 +24,15 @@
     </PropertyGroup>
     </PropertyGroup>
 
 
     <ItemGroup>
     <ItemGroup>
-      <PackageReference Include="SkiaSharp" Version="2.80.3" />
+      <PackageReference Include="SkiaSharp" Version="2.80.*" />
+    </ItemGroup>
+
+    <ItemGroup>
+        <InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
+        <InternalsVisibleTo Include="QuestPDF.ReportSample" />
+        <InternalsVisibleTo Include="QuestPDF.UnitTests" />
+        <InternalsVisibleTo Include="QuestPDF.Examples" />
+        <InternalsVisibleTo Include="QuestPDF.Previewer" />
     </ItemGroup>
     </ItemGroup>
 
 
     <ItemGroup>
     <ItemGroup>

+ 3 - 10
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.