Browse Source

Basic previewer implementation

MarcinZiabek 3 years ago
parent
commit
8551e1f33f

+ 80 - 0
QuestPDF.Previewer.Example/Program.cs

@@ -0,0 +1,80 @@
+using System.Diagnostics;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using QuestPDF.Previewer;
+
+Document
+    .Create(container =>
+    {
+        container.Page(page =>
+        {
+            page.Size(PageSizes.A4);
+            page.Margin(2, Unit.Centimetre);
+            page.PageColor(Colors.White);
+            page.DefaultTextStyle(x => x.FontSize(14));
+                        
+            page.Header()
+                .Text("Hello PDF!")
+                .SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
+
+            page.Content()
+                .PaddingVertical(1, Unit.Centimetre)
+                .Table(table =>
+                {
+                    table.ColumnsDefinition(columns =>
+                    {
+                        columns.ConstantColumn(30);
+                        columns.RelativeColumn();
+                        columns.ConstantColumn(75);
+                        columns.ConstantColumn(75);
+                        columns.ConstantColumn(75);
+                    });
+
+                    table.Header(header =>
+                    {
+                        var textStyle = TextStyle.Default.SemiBold();
+
+                        header.Cell().Element(DefaultCellStyle).Text("#").Style(textStyle);
+                        header.Cell().Element(DefaultCellStyle).Text("Item name").Style(textStyle);
+                        header.Cell().Element(DefaultCellStyle).AlignRight().Text("Price").Style(textStyle);
+                        header.Cell().Element(DefaultCellStyle).AlignRight().Text("Count").Style(textStyle);
+                        header.Cell().Element(DefaultCellStyle).AlignRight().Text("Total").Style(textStyle);
+
+                        static IContainer DefaultCellStyle(IContainer container)
+                        {
+                            return container.PaddingBottom(5).BorderBottom(1).PaddingBottom(5);
+                        }
+                    });
+
+                    foreach(var i in Enumerable.Range(1, 100))
+                    {
+                        var price = Placeholders.Random.Next(100, 999) / 100f;
+                        var count = Placeholders.Random.Next(1, 10);
+                        var total = price * count;
+
+                        table.Cell().Element(DefaultCellStyle).Text(i);
+                        table.Cell().Element(DefaultCellStyle).Text(Placeholders.Label());
+                        table.Cell().Element(DefaultCellStyle).AlignRight().Text($"{price:N2} $");
+                        table.Cell().Element(DefaultCellStyle).AlignRight().Text(count);
+                        table.Cell().Element(DefaultCellStyle).AlignRight().Text($"{total:N2} $");
+
+                        static IContainer DefaultCellStyle(IContainer container)
+                        {
+                            return container.PaddingBottom(5).BorderBottom(1).BorderColor(Colors.Grey.Lighten1).PaddingBottom(5);
+                        }
+                    }
+                });
+                        
+            page.Footer()
+                .AlignCenter()
+                .Text(x =>
+                {
+                    x.Span("Page ");
+                    x.CurrentPageNumber();
+                    x.Span(" of ");
+                    x.TotalPages();
+                });
+        });
+    })
+    .ShowInPreviewer();

+ 18 - 0
QuestPDF.Previewer.Example/QuestPDF.Previewer.Example.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <OutputType>Exe</OutputType>
+        <TargetFramework>net6.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <Compile Remove="HotReloadManager.cs" />
+    </ItemGroup>
+
+</Project>

+ 125 - 0
QuestPDF.Previewer/DocumentPreviewerExtensions.cs

@@ -0,0 +1,125 @@
+using System.Diagnostics;
+using System.Reflection;
+using Microsoft.AspNetCore.Builder;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer;
+
+public static class DocumentPreviewerExtensions
+{
+    public static void ShowInPreviewer(this IDocument document)
+    {
+        ArgumentNullException.ThrowIfNull(document);
+
+        var builder = WebApplication.CreateBuilder();
+        var app = builder.Build();
+        
+        var pdfDocumentCache = GeneratePdf(document);
+        var refreshFlag = false;
+
+        static byte[] GenerateDocumentAboutException(Exception exception)
+        {
+            return Document.Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A4);
+                    page.Margin(1, Unit.Inch);
+                    page.PageColor(Colors.Red.Lighten4);
+                    page.DefaultTextStyle(x => x.FontSize(16));
+
+                    page.Header()
+                        .BorderBottom(2)
+                        .BorderColor(Colors.Red.Medium)
+                        .PaddingBottom(5)
+                        .Text("Ooops! Something went wrong...").FontSize(28).FontColor(Colors.Red.Medium).Bold();
+
+                    page.Content().PaddingVertical(20).Column(column =>
+                    {
+                        var currentException = exception;
+
+                        while (currentException != null)
+                        {
+                            column.Item().Text(exception.GetType().Name).FontSize(20).SemiBold();
+                            column.Item().Text(exception.Message).FontSize(14);
+                            column.Item().PaddingTop(10).Text(exception.StackTrace).FontSize(10).Light();
+
+                            currentException = currentException.InnerException;
+
+                            if (currentException != null)
+                                column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
+                        }
+                    });
+                });
+            }).GeneratePdf();
+        }
+
+        static byte[] GeneratePdf(IDocument document)
+        {
+            try
+            {
+                return document.GeneratePdf();
+            }
+            catch(Exception exception)
+            {
+                return GenerateDocumentAboutException(exception);
+            }
+        }
+
+        HotReloadManager.OnApplicationChanged += () =>
+        {
+            pdfDocumentCache = GeneratePdf(document);
+            refreshFlag = true;
+        };
+  
+        app.MapGet("/", () =>
+        {
+            var assembly = Assembly.GetExecutingAssembly();
+            var resourceName = "QuestPDF.Previewer.index.html";
+        
+            using var stream = assembly.GetManifestResourceStream(resourceName);
+            using var reader = new StreamReader(stream);
+            var result = reader.ReadToEnd();
+            return Results.Content(result, "text/html");
+        });
+        
+        app.MapGet("/render", () =>
+        {
+            refreshFlag = false;
+            return Results.File(pdfDocumentCache, "application/pdf");
+        });
+        
+        app.MapGet("/listen", async () =>
+        {
+            foreach (var i in Enumerable.Range(0, 1000))
+            {
+                await Task.Delay(TimeSpan.FromMilliseconds(100));
+
+                if (!refreshFlag)
+                    continue;
+
+                return Results.Text("true");
+            }
+
+            return Results.Text("false");
+        });
+
+        app.Lifetime.ApplicationStarted.Register(() =>
+        {
+            var openBrowserProcess = new Process()
+            {
+                StartInfo = new()
+                {
+                    UseShellExecute = true,
+                    FileName = app.Urls.First()
+                }
+            };
+
+            openBrowserProcess.Start();
+        });
+
+        app.Run();
+    }
+}

+ 9 - 0
QuestPDF.Previewer/HotReloadManager.cs

@@ -0,0 +1,9 @@
+using System.Reflection.Metadata;
+
+[assembly: MetadataUpdateHandler(typeof(HotReloadManager))]
+
+internal static class HotReloadManager
+{
+    public static event Action? OnApplicationChanged;
+    public static void UpdateApplication(Type[]? _) => OnApplicationChanged?.Invoke();
+}

+ 24 - 0
QuestPDF.Previewer/QuestPDF.Previewer.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+    <PropertyGroup>
+        <TargetFramework>net6.0</TargetFramework>
+        <Nullable>enable</Nullable>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <OutputType>Library</OutputType>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <None Remove="index.html" />
+      <EmbeddedResource Include="index.html" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <Content Remove="appsettings.Development.json" />
+      <Content Remove="appsettings.json" />
+    </ItemGroup>
+    
+</Project>

+ 117 - 0
QuestPDF.Previewer/index.html

@@ -0,0 +1,117 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>QuestPDF Previewer</title>
+    
+    <style>
+        html {
+            background-color: #333;
+        }
+        
+        body {
+            margin: 0;
+        }
+
+        embed {
+            border: none;
+
+            position: absolute;
+
+            left: 0;
+            top: 0;
+            
+            width: 100%;
+            height: 100%;
+        }
+
+        .alert {
+            position: absolute;
+            top: 0;
+            left: 0;
+            right: 0;
+            width: 100%;
+            padding: 8px 16px;
+            color: #FFFA;
+            font-family: sans-serif;
+            font-size: 14px;
+            line-height: 1.5;
+            box-shadow: 0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12) !important;
+        }
+
+        #recommended-alert {
+            background-color: #616161;
+        }
+
+        #recommended-alert .close {
+            color: #FFF;
+            cursor: pointer;
+        }
+
+        #connection-issue-alert {
+            visibility: hidden;
+            background-color: #B71C1C;
+            color: white;
+        }
+    </style>
+</head>
+<body>
+
+<embed id="previewer" src="/render#toolbar=0&scrollbar=0" />
+
+<div id="recommended-alert" class="alert">
+    The recommended browser for this previewer is Microsoft Edge. It does preserve document scroll position when refreshing document, giving better overall experience.
+    <a class="close" onclick="closeRecommendation()">Click to close</a>
+</div>
+
+<div id="connection-issue-alert" class="alert">
+    Cannot connect to QuestPDF previewer host. Possibly your debugging session has ended. Please close this browser.
+</div>
+
+<script>
+    const recommendationAlert = document.getElementById("recommended-alert");
+    const connectionIssueAlert = document.getElementById("connection-issue-alert");
+
+    function closeRecommendation() {
+        recommendationAlert.parentElement.removeChild(recommendationAlert);
+        localStorage.setItem("show-recommendation-alert", 'false');
+    }
+
+    function refreshRecommendation() {
+        const showRecommendation = localStorage.getItem("show-recommendation-alert");
+
+        if (showRecommendation !== 'false')
+            return;
+
+        recommendationAlert.parentElement.removeChild(recommendationAlert);
+    }
+
+    function changeConnectionIssueAlertVisibility(show) {
+        connectionIssueAlert.style.setProperty("visibility", show ? "visible" : "hidden");
+    }
+
+    async function refreshDocumentLoop() {
+        window.addEventListener("change", () => reloading = true);
+
+        while (true) {
+            try {
+                const response = await fetch('/listen');
+                const value = await response.text();
+
+                if (value === "true")
+                    window.location.reload();
+
+                changeConnectionIssueAlertVisibility(false);
+            }
+            catch {
+                changeConnectionIssueAlertVisibility(true);
+            }
+        }
+    }
+
+    refreshRecommendation();
+    refreshDocumentLoop();
+</script>
+
+</body>
+</html>

+ 12 - 0
QuestPDF.sln

@@ -8,6 +8,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B4D3CBBC-2F38-434F-9347-6E5297737047}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Example", "QuestPDF.Previewer.Example\QuestPDF.Previewer.Example.csproj", "{012F05A0-C30B-4112-8EB0-E70CE1D31E57}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -30,5 +34,13 @@ Global
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
 		{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B4D3CBBC-2F38-434F-9347-6E5297737047}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B4D3CBBC-2F38-434F-9347-6E5297737047}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B4D3CBBC-2F38-434F-9347-6E5297737047}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B4D3CBBC-2F38-434F-9347-6E5297737047}.Release|Any CPU.Build.0 = Release|Any CPU
+		{012F05A0-C30B-4112-8EB0-E70CE1D31E57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{012F05A0-C30B-4112-8EB0-E70CE1D31E57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{012F05A0-C30B-4112-8EB0-E70CE1D31E57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{012F05A0-C30B-4112-8EB0-E70CE1D31E57}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 EndGlobal
 EndGlobal