From b80b3414d81f6d908cf416f58d2ca53bf1f58023 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 31 Mar 2022 15:44:28 +0200 Subject: [PATCH] Added http-based previewer --- QuestPDF.Previewer.Examples/Program.cs | 26 ++- QuestPDF.Previewer/CommunicationService.cs | 65 ++++++ QuestPDF.Previewer/Helpers.cs | 23 ++ QuestPDF.Previewer/InteractiveCanvas.cs | 202 ++++++++++++++++++ QuestPDF.Previewer/PreviewerApp.axaml | 7 + QuestPDF.Previewer/PreviewerApp.axaml.cs | 27 +++ QuestPDF.Previewer/PreviewerCanvas.cs | 6 + QuestPDF.Previewer/PreviewerControl.cs | 118 ++++++++++ QuestPDF.Previewer/PreviewerRefreshCommand.cs | 18 ++ QuestPDF.Previewer/PreviewerWindow.axaml | 82 +++++++ QuestPDF.Previewer/PreviewerWindow.axaml.cs | 23 ++ .../PreviewerWindowViewModel.cs | 89 ++++++++ QuestPDF.Previewer/Program.cs | 38 ++++ QuestPDF.Previewer/QuestPDF.Previewer.csproj | 31 +++ QuestPDF.Previewer/Resources/Logo.png | Bin 0 -> 14639 bytes QuestPDF.Previewer/readme.md | 23 ++ QuestPDF.sln | 12 +- QuestPDF/Previewer/ExceptionDocument.cs | 2 +- QuestPDF/Previewer/PreviewerService.cs | 2 +- 19 files changed, 779 insertions(+), 15 deletions(-) create mode 100644 QuestPDF.Previewer/CommunicationService.cs create mode 100644 QuestPDF.Previewer/Helpers.cs create mode 100644 QuestPDF.Previewer/InteractiveCanvas.cs create mode 100644 QuestPDF.Previewer/PreviewerApp.axaml create mode 100644 QuestPDF.Previewer/PreviewerApp.axaml.cs create mode 100644 QuestPDF.Previewer/PreviewerCanvas.cs create mode 100644 QuestPDF.Previewer/PreviewerControl.cs create mode 100644 QuestPDF.Previewer/PreviewerRefreshCommand.cs create mode 100644 QuestPDF.Previewer/PreviewerWindow.axaml create mode 100644 QuestPDF.Previewer/PreviewerWindow.axaml.cs create mode 100644 QuestPDF.Previewer/PreviewerWindowViewModel.cs create mode 100644 QuestPDF.Previewer/Program.cs create mode 100644 QuestPDF.Previewer/QuestPDF.Previewer.csproj create mode 100644 QuestPDF.Previewer/Resources/Logo.png create mode 100644 QuestPDF.Previewer/readme.md diff --git a/QuestPDF.Previewer.Examples/Program.cs b/QuestPDF.Previewer.Examples/Program.cs index 8a42c86..7d57716 100644 --- a/QuestPDF.Previewer.Examples/Program.cs +++ b/QuestPDF.Previewer.Examples/Program.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using System.Net.Http.Headers; +using Avalonia.Media; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; @@ -7,11 +8,13 @@ using QuestPDF.ReportSample; using QuestPDF.ReportSample.Layouts; using Colors = QuestPDF.Helpers.Colors; -var model = DataSource.GetReport(); -var report = new StandardReport(model); -report.ShowInPreviewer(); +ImagePlaceholder.Solid = true; -return; +// var model = DataSource.GetReport(); +// var report = new StandardReport(model); +// report.ShowInPreviewer().Wait(); +// +// return; Document .Create(container => @@ -48,6 +51,16 @@ Document }); x.Item().Text("Modify this line and the preview should show your changes instantly."); + + // for testing exception handling + // try + // { + // throw new ArgumentException("This file does not exists... peace.png"); + // } + // catch (Exception e) + // { + // throw new FileNotFoundException("This is the top exception!", e); + // } }); page.Footer() @@ -63,7 +76,6 @@ Document { page.Size(PageSizes.A4); page.Margin(2, Unit.Centimetre); - page.PageColor(Colors.Red.Medium); page.DefaultTextStyle(x => x.FontSize(20)); page.Content() @@ -77,4 +89,4 @@ Document }); }); }) - .ShowInPreviewer(); \ No newline at end of file + .ShowInPreviewer().Wait(); \ No newline at end of file diff --git a/QuestPDF.Previewer/CommunicationService.cs b/QuestPDF.Previewer/CommunicationService.cs new file mode 100644 index 0000000..2a25863 --- /dev/null +++ b/QuestPDF.Previewer/CommunicationService.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SkiaSharp; + +namespace QuestPDF.Previewer; + +class CommunicationService +{ + public static CommunicationService Instance { get; } = new (); + + public event Action> OnDocumentRefreshed; + + private WebApplication? Application { get; set; } + + private readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + private CommunicationService() + { + + } + + public Task Start(int port) + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddLogging(x => x.ClearProviders()); + Application = builder.Build(); + + Application.MapGet("ping", () => Results.Ok()); + Application.MapGet("version", () => Results.Ok(GetType().Assembly.GetName().Version)); + Application.MapPost("update/preview", HandleUpdatePreview); + + return Application.RunAsync($"http://localhost:{port}/"); + } + + public async Task Stop() + { + await Application.StopAsync(); + await Application.DisposeAsync(); + } + + private async Task HandleUpdatePreview(HttpRequest request) + { + var command = JsonSerializer.Deserialize(request.Form["command"], JsonSerializerOptions); + + var pages = command + .Pages + .Select(page => + { + using var stream = request.Form.Files[page.Id].OpenReadStream(); + var picture = SKPicture.Deserialize(stream); + + return new PreviewPage(picture, page.Width, page.Height); + }) + .ToList(); + + OnDocumentRefreshed(pages); + return Results.Ok(); + } +} \ No newline at end of file diff --git a/QuestPDF.Previewer/Helpers.cs b/QuestPDF.Previewer/Helpers.cs new file mode 100644 index 0000000..fe0a809 --- /dev/null +++ b/QuestPDF.Previewer/Helpers.cs @@ -0,0 +1,23 @@ +using SkiaSharp; + +namespace QuestPDF.Previewer; + +class Helpers +{ + public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection pages) + { + using var stream = File.Create(filePath); + + var document = SKDocument.CreatePdf(stream); + + foreach (var page in pages) + { + using var canvas = document.BeginPage(page.Width, page.Height); + canvas.DrawPicture(page.Picture); + document.EndPage(); + canvas.Dispose(); + } + + document.Close(); + } +} \ No newline at end of file diff --git a/QuestPDF.Previewer/InteractiveCanvas.cs b/QuestPDF.Previewer/InteractiveCanvas.cs new file mode 100644 index 0000000..80db1cb --- /dev/null +++ b/QuestPDF.Previewer/InteractiveCanvas.cs @@ -0,0 +1,202 @@ +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using SkiaSharp; + +namespace QuestPDF.Previewer; + +class InteractiveCanvas : ICustomDrawOperation +{ + public Rect Bounds { get; set; } + public ICollection Pages { get; set; } + + private float Width => (float)Bounds.Width; + private float Height => (float)Bounds.Height; + + public float Scale { get; private set; } = 1; + public float TranslateX { get; set; } + public float TranslateY { get; set; } + + private const float MinScale = 0.1f; + private const float MaxScale = 10f; + + private const float PageSpacing = 25f; + private const float SafeZone = 25f; + + public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing; + public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale; + public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0; + + public float MaxTranslateY => TotalHeight - Height / Scale; + + public float ScrollPercentY + { + get + { + return TranslateY / MaxTranslateY; + } + set + { + TranslateY = value * MaxTranslateY; + } + } + + public float ScrollViewportSizeY + { + get + { + var viewPortSize = Height / Scale / TotalHeight; + return Math.Clamp(viewPortSize, 0, 1); + } + } + + #region transformations + + private void LimitScale() + { + Scale = Math.Max(Scale, MinScale); + Scale = Math.Min(Scale, MaxScale); + } + + private void LimitTranslate() + { + if (TotalPagesHeight > Height / Scale) + { + TranslateY = Math.Min(TranslateY, MaxTranslateY); + TranslateY = Math.Max(TranslateY, 0); + } + else + { + TranslateY = (TotalPagesHeight - Height / Scale) / 2; + } + + if (Width / Scale < MaxWidth) + { + var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2; + + TranslateX = Math.Min(TranslateX, -maxTranslateX); + TranslateX = Math.Max(TranslateX, maxTranslateX); + } + else + { + TranslateX = 0; + } + } + + public void TranslateWithCurrentScale(float x, float y) + { + TranslateX += x / Scale; + TranslateY += y / Scale; + + LimitTranslate(); + } + + public void ZoomToPoint(float x, float y, float factor) + { + var oldScale = Scale; + Scale *= factor; + + LimitScale(); + + TranslateX -= x / Scale - x / oldScale; + TranslateY -= y / Scale - y / oldScale; + + LimitTranslate(); + } + + #endregion + + #region rendering + + public void Render(IDrawingContextImpl context) + { + if (Pages.Count <= 0) + return; + + LimitScale(); + LimitTranslate(); + + var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas; + + if (canvas == null) + throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}"); + + var originalMatrix = canvas.TotalMatrix; + + canvas.Translate(Width / 2, 0); + + canvas.Scale(Scale); + canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale); + + foreach (var page in Pages) + { + canvas.Translate(-page.Width / 2f, 0); + DrawBlankPage(canvas, page.Width, page.Height); + canvas.DrawPicture(page.Picture); + canvas.Translate(page.Width / 2f, page.Height + PageSpacing); + } + + canvas.SetMatrix(originalMatrix); + DrawInnerGradient(canvas); + } + + public void Dispose() { } + public bool Equals(ICustomDrawOperation? other) => false; + public bool HitTest(Point p) => true; + + #endregion + + #region blank page + + private static SKPaint BlankPagePaint = new SKPaint + { + Color = SKColors.White + }; + + private static SKPaint BlankPageShadowPaint = new SKPaint + { + ImageFilter = SKImageFilter.CreateBlendMode( + SKBlendMode.Overlay, + SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64)), + SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32))) + }; + + private void DrawBlankPage(SKCanvas canvas, float width, float height) + { + canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint); + canvas.DrawRect(0, 0, width, height, BlankPagePaint); + } + + #endregion + + #region inner viewport gradient + + private const int InnerGradientSize = (int)SafeZone; + private static readonly SKColor InnerGradientColor = SKColor.Parse("#666"); + + private void DrawInnerGradient(SKCanvas canvas) + { + // gamma correction + var colors = Enumerable + .Range(0, InnerGradientSize) + .Select(x => 1f - x / (float) InnerGradientSize) + .Select(x => Math.Pow(x, 2f)) + .Select(x => (byte)(x * 255)) + .Select(x => InnerGradientColor.WithAlpha(x)) + .ToArray(); + + using var fogPaint = new SKPaint + { + Shader = SKShader.CreateLinearGradient( + new SKPoint(0, 0), + new SKPoint(0, InnerGradientSize), + colors, + SKShaderTileMode.Clamp) + }; + + canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint); + } + + #endregion +} diff --git a/QuestPDF.Previewer/PreviewerApp.axaml b/QuestPDF.Previewer/PreviewerApp.axaml new file mode 100644 index 0000000..725db69 --- /dev/null +++ b/QuestPDF.Previewer/PreviewerApp.axaml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/QuestPDF.Previewer/PreviewerApp.axaml.cs b/QuestPDF.Previewer/PreviewerApp.axaml.cs new file mode 100644 index 0000000..526d0ff --- /dev/null +++ b/QuestPDF.Previewer/PreviewerApp.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace QuestPDF.Previewer +{ + internal class PreviewerApp : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new PreviewerWindow() + { + DataContext = new PreviewerWindowViewModel() + }; + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerCanvas.cs b/QuestPDF.Previewer/PreviewerCanvas.cs new file mode 100644 index 0000000..5b25fdc --- /dev/null +++ b/QuestPDF.Previewer/PreviewerCanvas.cs @@ -0,0 +1,6 @@ +using SkiaSharp; + +namespace QuestPDF.Previewer +{ + record PreviewPage(SKPicture Picture, float Width, float Height); +} diff --git a/QuestPDF.Previewer/PreviewerControl.cs b/QuestPDF.Previewer/PreviewerControl.cs new file mode 100644 index 0000000..b433748 --- /dev/null +++ b/QuestPDF.Previewer/PreviewerControl.cs @@ -0,0 +1,118 @@ +using System.Collections.ObjectModel; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Media; + +namespace QuestPDF.Previewer +{ + class PreviewerControl : Control + { + private InteractiveCanvas InteractiveCanvas { get; set; } = new (); + + public static readonly StyledProperty> PagesProperty = + AvaloniaProperty.Register>(nameof(Pages)); + + public ObservableCollection? Pages + { + get => GetValue(PagesProperty); + set => SetValue(PagesProperty, value); + } + + public static readonly StyledProperty CurrentScrollProperty = AvaloniaProperty.Register(nameof(CurrentScroll)); + + public float CurrentScroll + { + get => GetValue(CurrentScrollProperty); + set => SetValue(CurrentScrollProperty, value); + } + + public static readonly StyledProperty ScrollViewportSizeProperty = AvaloniaProperty.Register(nameof(ScrollViewportSize)); + + public float ScrollViewportSize + { + get => GetValue(ScrollViewportSizeProperty); + set => SetValue(ScrollViewportSizeProperty, value); + } + + public PreviewerControl() + { + PagesProperty.Changed.Subscribe(x => + { + InteractiveCanvas.Pages = x.NewValue.Value; + InvalidateVisual(); + }); + + CurrentScrollProperty.Changed.Subscribe(x => + { + InteractiveCanvas.ScrollPercentY = x.NewValue.Value; + InvalidateVisual(); + }); + + ClipToBounds = true; + } + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + + if ((e.KeyModifiers & KeyModifiers.Control) != 0) + { + var scaleFactor = 1 + e.Delta.Y / 10f; + var point = new Point(Bounds.Center.X, Bounds.Top) - e.GetPosition(this); + + InteractiveCanvas.ZoomToPoint((float)point.X, -(float)point.Y, (float)scaleFactor); + } + + if (e.KeyModifiers == KeyModifiers.None) + { + var translation = (float)e.Delta.Y * 25; + InteractiveCanvas.TranslateWithCurrentScale(0, -translation); + } + + InvalidateVisual(); + } + + private bool IsMousePressed { get; set; } + private Vector MousePosition { get; set; } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + + if (IsMousePressed) + { + var currentPosition = e.GetPosition(this); + var translation = currentPosition - MousePosition; + InteractiveCanvas.TranslateWithCurrentScale((float)translation.X, -(float)translation.Y); + + InvalidateVisual(); + } + + MousePosition = e.GetPosition(this); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + IsMousePressed = true; + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + IsMousePressed = false; + } + + public override void Render(DrawingContext context) + { + CurrentScroll = InteractiveCanvas.ScrollPercentY; + ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY; + + InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height); + + context.Custom(InteractiveCanvas); + base.Render(context); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerRefreshCommand.cs b/QuestPDF.Previewer/PreviewerRefreshCommand.cs new file mode 100644 index 0000000..3554585 --- /dev/null +++ b/QuestPDF.Previewer/PreviewerRefreshCommand.cs @@ -0,0 +1,18 @@ +using SkiaSharp; + +namespace QuestPDF.Previewer; + +internal class DocumentSnapshot +{ + public ICollection Pages { get; set; } + + public class PageSnapshot + { + public string Id { get; set; } + + public float Width { get; set; } + public float Height { get; set; } + + public SKPicture Picture { get; set; } + } +} diff --git a/QuestPDF.Previewer/PreviewerWindow.axaml b/QuestPDF.Previewer/PreviewerWindow.axaml new file mode 100644 index 0000000..3038e5e --- /dev/null +++ b/QuestPDF.Previewer/PreviewerWindow.axaml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/QuestPDF.Previewer/PreviewerWindow.axaml.cs b/QuestPDF.Previewer/PreviewerWindow.axaml.cs new file mode 100644 index 0000000..2eb5262 --- /dev/null +++ b/QuestPDF.Previewer/PreviewerWindow.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace QuestPDF.Previewer +{ + class PreviewerWindow : Window + { + public PreviewerWindow() + { + InitializeComponent(); + } + + protected override void OnClosed(EventArgs e) + { + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerWindowViewModel.cs b/QuestPDF.Previewer/PreviewerWindowViewModel.cs new file mode 100644 index 0000000..4246ed5 --- /dev/null +++ b/QuestPDF.Previewer/PreviewerWindowViewModel.cs @@ -0,0 +1,89 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using ReactiveUI; +using Unit = System.Reactive.Unit; +using Avalonia.Threading; + +namespace QuestPDF.Previewer +{ + internal class PreviewerWindowViewModel : ReactiveObject + { + private ObservableCollection _pages = new(); + public ObservableCollection Pages + { + get => _pages; + set => this.RaiseAndSetIfChanged(ref _pages, value); + } + + private float _currentScroll; + public float CurrentScroll + { + get => _currentScroll; + set => this.RaiseAndSetIfChanged(ref _currentScroll, value); + } + + private float _scrollViewportSize; + public float ScrollViewportSize + { + get => _scrollViewportSize; + set + { + this.RaiseAndSetIfChanged(ref _scrollViewportSize, value); + VerticalScrollbarVisible = value < 1; + } + } + + private bool _verticalScrollbarVisible; + public bool VerticalScrollbarVisible + { + get => _verticalScrollbarVisible; + private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value)); + } + + public ReactiveCommand ShowPdfCommand { get; } + public ReactiveCommand ShowDocumentationCommand { get; } + public ReactiveCommand SponsorProjectCommand { get; } + + public PreviewerWindowViewModel() + { + CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview; + + ShowPdfCommand = ReactiveCommand.Create(ShowPdf); + ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html")); + SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF")); + } + + private void ShowPdf() + { + var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf"); + Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages); + + OpenLink(filePath); + } + + private void OpenLink(string path) + { + var openBrowserProcess = new Process + { + StartInfo = new() + { + UseShellExecute = true, + FileName = path + } + }; + + openBrowserProcess.Start(); + } + + private void HandleUpdatePreview(ICollection pages) + { + var oldPages = Pages; + + Pages.Clear(); + Pages = new ObservableCollection(pages); + + foreach (var page in oldPages) + page.Picture.Dispose(); + } + } +} diff --git a/QuestPDF.Previewer/Program.cs b/QuestPDF.Previewer/Program.cs new file mode 100644 index 0000000..71c7e96 --- /dev/null +++ b/QuestPDF.Previewer/Program.cs @@ -0,0 +1,38 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.ReactiveUI; +using QuestPDF.Previewer; + +var applicationPort = GetCommunicationPort(); +CommunicationService.Instance.Start(applicationPort); + +if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop) +{ + desktop.MainWindow = new PreviewerWindow() + { + DataContext = new PreviewerWindowViewModel() + }; + + desktop.MainWindow.Show(); + desktop.Start(Array.Empty()); + + return; +} + +AppBuilder + .Configure(() => new PreviewerApp()) + .UsePlatformDetect() + .UseReactiveUI() + .StartWithClassicDesktopLifetime(Array.Empty()); + +static int GetCommunicationPort() +{ + const int defaultApplicationPort = 12500; + + var arguments = Environment.GetCommandLineArgs(); + + if (arguments.Length < 2) + return defaultApplicationPort; + + return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort; +} \ No newline at end of file diff --git a/QuestPDF.Previewer/QuestPDF.Previewer.csproj b/QuestPDF.Previewer/QuestPDF.Previewer.csproj new file mode 100644 index 0000000..0b0748c --- /dev/null +++ b/QuestPDF.Previewer/QuestPDF.Previewer.csproj @@ -0,0 +1,31 @@ + + + + exe + net6.0 + enable + enable + + true + QuestPDF.Previewer + 2022.4.0 + true + questpdf-previewer + + + + + + + + + + + + + + + + + + diff --git a/QuestPDF.Previewer/Resources/Logo.png b/QuestPDF.Previewer/Resources/Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a0f1a1915bb89cb3842c5dd0daf8d346dad5410f GIT binary patch literal 14639 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 P?;O>YwUuh_+eG~zo88u) literal 0 HcmV?d00001 diff --git a/QuestPDF.Previewer/readme.md b/QuestPDF.Previewer/readme.md new file mode 100644 index 0000000..79ed13e --- /dev/null +++ b/QuestPDF.Previewer/readme.md @@ -0,0 +1,23 @@ +Install nuget locally (in directory where nupkg file is located) + +``` +dotnet tool install --global --add-source . QuestPDF.Previewer --global +``` + +Run on default port + +``` +questpdf-previewer +``` + +Run on custom port + +``` +questpdf-previewer 12500 +``` + +Remove nuget locally + +``` +dotnet tool uninstall QuestPDF.Previewer --global +``` \ No newline at end of file diff --git a/QuestPDF.sln b/QuestPDF.sln index 87eff4d..cf35650 100644 --- a/QuestPDF.sln +++ b/QuestPDF.sln @@ -8,8 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}" @@ -17,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configurat .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,13 +39,13 @@ Global {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.Build.0 = Release|Any CPU - {D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.Build.0 = Release|Any CPU {CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU {CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU + {B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/QuestPDF/Previewer/ExceptionDocument.cs b/QuestPDF/Previewer/ExceptionDocument.cs index 266a02b..f3f025c 100644 --- a/QuestPDF/Previewer/ExceptionDocument.cs +++ b/QuestPDF/Previewer/ExceptionDocument.cs @@ -81,7 +81,7 @@ namespace QuestPDF.Previewer .Text(text => { - text.DefaultTextStyle(x => x.FontSize(18)); + text.DefaultTextStyle(x => x.FontSize(16)); text.Span(currentException.GetType().Name + ": ").Bold(); text.Span(currentException.Message); diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index e2abe06..123b2b0 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -63,7 +63,7 @@ namespace QuestPDF.Previewer StartInfo = new() { UseShellExecute = false, - FileName = "questpdf-test", + FileName = "questpdf-previewer", CreateNoWindow = true } };