Compare commits

...

18 Commits

Author SHA1 Message Date
MarcinZiabek 03e3a80fd9 Fixed logo resource for QuestPDF.Previwer 2022-04-04 23:26:16 +02:00
MarcinZiabek 4692c5ce62 Fixed QuestPDF.Previewer build 2022-04-04 23:24:02 +02:00
MarcinZiabek 919f916219 2022.4 release 2022-04-04 23:20:22 +02:00
MarcinZiabek 7a32f774a1 Fixed spelling 2022-04-04 00:38:58 +02:00
MarcinZiabek fba71e6392 Previewer: stop debugging session when previewer is closed 2022-04-04 00:19:36 +02:00
MarcinZiabek e27585a504 Fixed previewer: first document refresh sometimes does not work 2022-04-02 00:23:58 +02:00
MarcinZiabek 9be497ee41 Fixed automatically starting previewer application on custom port 2022-04-01 19:26:54 +02:00
MarcinZiabek 0fc0db55d2 Better button style path in previewer 2022-03-31 23:14:00 +02:00
MarcinZiabek 9e7a1ed49d Previewer improvements 2022-03-31 23:11:34 +02:00
MarcinZiabek c137bcfcde Improved look and feel 2022-03-31 18:33:34 +02:00
MarcinZiabek b80b3414d8 Added http-based previewer 2022-03-31 15:44:28 +02:00
MarcinZiabek 5f7cee3420 Removed old previewer implementation 2022-03-30 23:38:51 +02:00
MarcinZiabek 435c42ab9e Changed previewer communication layer to http 2022-03-30 23:37:37 +02:00
MarcinZiabek 7b4923b45d Merge remote-tracking branch 'origin/previewer' 2022-03-27 20:15:02 +02:00
MarcinZiabek 8732b7e50b Renaming 2022-03-27 20:12:22 +02:00
Marcin Ziąbek a0909eb057 Create FUNDING.yml 2022-03-26 21:06:44 +01:00
MarcinZiabek 11ff53946d Introduced new line breaking algorithm: break anywhere 2022-03-26 03:34:31 +01:00
MarcinZiabek 0711bca1e1 Improved respecting spaces at the beginning and end of text block, fixed breaking long words that do not fit in line 2022-03-26 02:58:55 +01:00
40 changed files with 829 additions and 402 deletions
+1
View File
@@ -0,0 +1 @@
github: QuestPDF
+32
View File
@@ -371,5 +371,37 @@ namespace QuestPDF.Examples
});
});
}
[Test]
public void BreakingLongWord()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.Size(PageSizes.A4);
page.Content().Column(column =>
{
column.Item().Text(null);
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
text.Span(" " + Placeholders.LoremIpsum());
text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
});
});
});
});
}
}
}
+7 -26
View File
@@ -1,17 +1,16 @@
using Avalonia.Media;
using QuestPDF.Fluent;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
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 =>
@@ -58,23 +57,5 @@ Document
x.CurrentPageNumber();
});
});
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.Red.Medium);
page.DefaultTextStyle(x => x.FontSize(20));
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
foreach (var i in Enumerable.Range(0, 10))
x.Item().Background(Colors.Grey.Lighten2).Height(80);
});
});
})
.ShowInPreviewer();
@@ -8,7 +8,6 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
@@ -0,0 +1,77 @@
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<ICollection<PreviewPage>>? 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", HandlePing);
Application.MapGet("version", HandleVersion);
Application.MapPost("update/preview", HandleUpdatePreview);
return Application.RunAsync($"http://localhost:{port}/");
}
public async Task Stop()
{
await Application.StopAsync();
await Application.DisposeAsync();
}
private async Task<IResult> HandlePing()
{
return OnDocumentRefreshed == null
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
: Results.Ok();
}
private async Task<IResult> HandleVersion()
{
return Results.Json(GetType().Assembly.GetName().Version);
}
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
{
var command = JsonSerializer.Deserialize<DocumentSnapshot>(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();
Task.Run(() => OnDocumentRefreshed(pages));
return Results.Ok();
}
}
@@ -1,50 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
public static class DocumentPreviewerExtensions
{
/// <summary>
/// Opens document in the QuestPDF previewer tool.
/// Improves development speed by supporting hot reloading.
/// Shows document preview and refreshes it after each code change.
/// </summary>
/// <remarks>
/// Intended for development only. Do not use in production environment.
/// </remarks>
public static void ShowInPreviewer(this IDocument document)
{
ArgumentNullException.ThrowIfNull(document);
// currently there is no way to utilize a previously run Avalonia app.
// so we need to check if the previewer was already run and show the window again.
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
{
Document = document,
}
};
desktop.MainWindow.Show();
desktop.Start(Array.Empty<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp()
{
Document = document,
})
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
}
}
}
-63
View File
@@ -1,63 +0,0 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using Avalonia.Threading;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
internal class DocumentRenderer : INotifyPropertyChanged
{
public IDocument? Document { get; private set; }
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _pages;
set
{
if (_pages != value)
{
_pages = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pages)));
}
}
}
public void UpdateDocument(IDocument? document)
{
Document = document;
if (document == null)
return;
try
{
RenderDocument(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
RenderDocument(exceptionDocument);
}
}
private void RenderDocument(IDocument document)
{
var canvas = new PreviewerCanvas();
DocumentGenerator.RenderDocument(canvas, document);
foreach (var pages in Pages)
pages?.Picture?.Dispose();
Dispatcher.UIThread.Post(() =>
{
Pages.Clear();
Pages = new ObservableCollection<PreviewPage>(canvas.Pictures);
});
}
}
}
-55
View File
@@ -1,55 +0,0 @@
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
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(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(currentException.GetType().Name).FontSize(20).SemiBold();
column.Item().Text(currentException.Message).FontSize(14);
column.Item().PaddingTop(10).Text(currentException.StackTrace).FontSize(10).Light();
currentException = currentException.InnerException;
if (currentException != null)
column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
}
});
});
}
}
+23
View File
@@ -0,0 +1,23 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
class Helpers
{
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> 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();
}
}
+12 -15
View File
@@ -1,5 +1,4 @@
using System.Diagnostics;
using Avalonia;
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
@@ -25,9 +24,9 @@ class InteractiveCanvas : ICustomDrawOperation
private const float PageSpacing = 25f;
private const float SafeZone = 25f;
public float TotalPagesHeight => Pages.Sum(x => x.Size.Height) + (Pages.Count - 1) * PageSpacing;
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.Size.Width) : 0;
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
public float MaxTranslateY => TotalHeight - Height / Scale;
@@ -99,11 +98,9 @@ class InteractiveCanvas : ICustomDrawOperation
Scale *= factor;
LimitScale();
factor = Scale / oldScale;
TranslateX -= x / (oldScale * factor) - x / oldScale;
TranslateY -= y / (oldScale * factor) - y / oldScale;
TranslateX -= x / Scale - x / oldScale;
TranslateY -= y / Scale - y / oldScale;
LimitTranslate();
}
@@ -134,10 +131,10 @@ class InteractiveCanvas : ICustomDrawOperation
foreach (var page in Pages)
{
canvas.Translate(-page.Size.Width / 2f, 0);
DrawBlankPage(canvas, page.Size);
canvas.Translate(-page.Width / 2f, 0);
DrawBlankPage(canvas, page.Width, page.Height);
canvas.DrawPicture(page.Picture);
canvas.Translate(page.Size.Width / 2f, page.Size.Height + PageSpacing);
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
}
canvas.SetMatrix(originalMatrix);
@@ -165,10 +162,10 @@ class InteractiveCanvas : ICustomDrawOperation
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
};
private void DrawBlankPage(SKCanvas canvas, QuestPDF.Infrastructure.Size size)
private void DrawBlankPage(SKCanvas canvas, float width, float height)
{
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPageShadowPaint);
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPagePaint);
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
}
#endregion
-6
View File
@@ -1,14 +1,11 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
internal class PreviewerApp : Application
{
public IDocument? Document { get; init; }
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
@@ -21,9 +18,6 @@ namespace QuestPDF.Previewer
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
{
Document = Document,
}
};
}
+2 -38
View File
@@ -1,42 +1,6 @@
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, Size Size);
sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
{
private SKPictureRecorder? PictureRecorder { get; set; }
private Size? CurrentPageSize { get; set; }
public ICollection<PreviewPage> Pictures { get; } = new List<PreviewPage>();
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 PreviewPage(picture, CurrentPageSize.Value));
PictureRecorder?.Dispose();
PictureRecorder = null;
}
public override void EndDocument() { }
}
record PreviewPage(SKPicture Picture, float Width, float Height);
}
-2
View File
@@ -1,10 +1,8 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using ReactiveUI;
namespace QuestPDF.Previewer
{
@@ -0,0 +1,18 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
internal class DocumentSnapshot
{
public ICollection<PageSnapshot> 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; }
}
}
+33 -21
View File
@@ -1,17 +1,32 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.PreviewerWindow"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
Icon="/Images/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.PreviewerWindow"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector="Button.actions">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="CornerRadius" Value="100"/>
<Setter Property="Background" Value="#888"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#999"/>
</Style>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
@@ -33,11 +48,10 @@
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
Pages="{Binding DocumentRenderer.Pages, Mode=OneWay}" />
Pages="{Binding Pages, Mode=OneWay}" />
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32" ZIndex="100">
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
<Button Classes="actions"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
<Viewbox Width="24" Height="24">
@@ -47,8 +61,7 @@
</Viewbox>
</Button>
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
<Button Classes="actions"
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
ToolTip.Tip="Opens official QuestPDF documentation">
<Viewbox Width="24" Height="24">
@@ -58,8 +71,7 @@
</Viewbox>
</Button>
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
Padding="10" CornerRadius="100"
<Button Classes="actions"
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
<Viewbox Width="24" Height="24">
+1 -1
View File
@@ -12,7 +12,7 @@ namespace QuestPDF.Previewer
protected override void OnClosed(EventArgs e)
{
(DataContext as PreviewerWindowViewModel)?.UnregisterHotReloadHandler();
}
private void InitializeComponent()
+19 -38
View File
@@ -1,7 +1,6 @@
using QuestPDF.Fluent;
using System.Collections.ObjectModel;
using System.Diagnostics;
using ReactiveUI;
using QuestPDF.Infrastructure;
using Unit = System.Reactive.Unit;
using Avalonia.Threading;
@@ -9,19 +8,13 @@ namespace QuestPDF.Previewer
{
internal class PreviewerWindowViewModel : ReactiveObject
{
public DocumentRenderer DocumentRenderer { get; } = new();
private IDocument? _document;
public IDocument? Document
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _document;
set
{
this.RaiseAndSetIfChanged(ref _document, value);
UpdateDocument(value);
}
get => _pages;
set => this.RaiseAndSetIfChanged(ref _pages, value);
}
private float _currentScroll;
public float CurrentScroll
{
@@ -53,40 +46,17 @@ namespace QuestPDF.Previewer
public PreviewerWindowViewModel()
{
HotReloadManager.UpdateApplicationRequested += InvalidateDocument;
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"));
}
public void UnregisterHotReloadHandler()
{
HotReloadManager.UpdateApplicationRequested -= InvalidateDocument;
}
private void InvalidateDocument(object? sender, EventArgs e)
{
UpdateDocument(Document);
}
private Task UpdateDocument(IDocument? document)
{
return Task.Run(() => DocumentRenderer.UpdateDocument(document));
}
private void ShowPdf()
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
try
{
Document?.GeneratePdf(filePath);
}
catch (Exception exception)
{
new ExceptionDocument(exception).GeneratePdf(filePath);
}
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
OpenLink(filePath);
}
@@ -104,5 +74,16 @@ namespace QuestPDF.Previewer
openBrowserProcess.Start();
}
private void HandleUpdatePreview(ICollection<PreviewPage> pages)
{
var oldPages = Pages;
Pages.Clear();
Pages = new ObservableCollection<PreviewPage>(pages);
foreach (var page in oldPages)
page.Picture.Dispose();
}
}
}
+38
View File
@@ -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<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp())
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
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;
}
+50 -23
View File
@@ -1,27 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.4.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName>
<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>Initial release.</PackageReleaseNotes>
<LangVersion>10</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon>
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core previewer</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<OutputType>exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Images\Logo.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
<ItemGroup>
<AvaloniaResource Include="Resources\Logo.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Logo.png" />
<None Include="Resources\Logo.png">
<Pack>true</Pack>
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Avalonia" Version="0.10.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
</ItemGroup>
</Project>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+23
View File
@@ -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
```
+6 -6
View File
@@ -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
-7
View File
@@ -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
View File
@@ -51,6 +51,13 @@ namespace QuestPDF.Drawing
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)
where TCanvas : ICanvas, IRenderingCanvas
{
+55
View File
@@ -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() { }
}
}
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Calculation
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
public bool IsFirstElementInBlock { get; set; }
public bool IsFirstElementInLine { get; set; }
}
}
@@ -17,6 +17,6 @@ namespace QuestPDF.Elements.Text.Calculation
public int NextIndex { get; set; }
public int TotalIndex { get; set; }
public bool IsLast => NextIndex == EndIndex;
public bool IsLast => EndIndex == TotalIndex;
}
}
+31 -31
View File
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
private const char Space = ' ';
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
@@ -26,8 +28,6 @@ namespace QuestPDF.Elements.Text.Items
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
const char space = ' ';
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
@@ -35,9 +35,9 @@ namespace QuestPDF.Elements.Text.Items
// if the element is the first one within the line,
// ignore leading spaces
if (request.IsFirstElementInLine)
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex < Text.Length && Text[startIndex] == space)
while (startIndex < Text.Length && Text[startIndex] == Space)
startIndex++;
}
@@ -67,17 +67,12 @@ namespace QuestPDF.Elements.Text.Items
if (wrappedTextLength == null)
return null;
textLength = wrappedTextLength.Value;
textLength = wrappedTextLength.Value.fragmentLength;
text = text.Slice(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
// when breaking text, omit spaces at the end of the line
while (nextIndex < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
@@ -92,34 +87,39 @@ namespace QuestPDF.Elements.Text.Items
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = nextIndex,
NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
TotalIndex = Text.Length
};
static int? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
{
// textLength - length of the part of the text that fits in available width (creating a line)
}
// TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
{
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (textLength == text.Length)
return textLength;
// entire text fits, no need to wrap
if (textLength == text.Length)
return (textLength, textLength + 1);
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != space && text[textLength] == space)
return textLength;
// breaking anywhere
if (Style.WrapAnywhere ?? false)
return (textLength, textLength);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space);
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != Space && text[textLength] == Space)
return (textLength, textLength + 1);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return lastSpaceIndex;
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return (lastSpaceIndex, lastSpaceIndex + 1);
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? textLength : null;
}
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? (textLength, textLength + 1) : null;
}
public virtual void Draw(TextDrawingRequest request)
+3 -1
View File
@@ -108,7 +108,7 @@ namespace QuestPDF.Elements.Text
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.EndIndex;
if (!RenderingQueue.Any())
ResetState();
@@ -170,6 +170,8 @@ namespace QuestPDF.Elements.Text
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstElementInBlock = currentElement == Items.First(),
IsFirstElementInLine = !currentLineElements.Any()
};
@@ -63,6 +63,12 @@ namespace QuestPDF.Fluent
return descriptor;
}
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.WrapAnywhere = value;
return descriptor;
}
#region Weight
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
+5
View File
@@ -71,6 +71,11 @@ namespace QuestPDF.Fluent
{
return style.Mutate(x => x.HasUnderline = value);
}
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.WrapAnywhere = value);
}
#region Weight
+5 -1
View File
@@ -16,6 +16,7 @@ namespace QuestPDF.Infrastructure
internal bool? IsItalic { get; set; }
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; }
internal object PaintKey { get; private set; }
internal object FontMetricsKey { get; private set; }
@@ -30,7 +31,8 @@ namespace QuestPDF.Infrastructure
FontWeight = Infrastructure.FontWeight.Normal,
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false
HasUnderline = false,
WrapAnywhere = false
};
public static TextStyle Default => new TextStyle();
@@ -58,6 +60,7 @@ namespace QuestPDF.Infrastructure
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
}
internal void OverrideStyle(TextStyle parentStyle)
@@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
}
internal TextStyle Clone()
+108
View File
@@ -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.ContinuousSize(PageSizes.A4.Width);
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(16));
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;
}
});
});
}
}
}
@@ -1,4 +1,8 @@
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
#if NET6_0_OR_GREATER
using System;
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
namespace QuestPDF.Previewer
{
@@ -15,3 +19,5 @@ namespace QuestPDF.Previewer
}
}
}
#endif
+66
View File
@@ -0,0 +1,66 @@
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
{
public static class Extensions
{
public static void ShowInPreviewer(this IDocument document, int port = 12500)
{
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
}
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
{
var previewerService = new PreviewerService(port);
var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
await RefreshPreview();
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
await WaitForPreviewerExit(cancellationTokenSource.Token);
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);
}
}
}
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
}
}
#endif
@@ -0,0 +1,23 @@
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; } = Guid.NewGuid().ToString("N");
public float Width { get; init; }
public float Height { get; init; }
}
}
}
#endif
+151
View File
@@ -0,0 +1,151 @@
#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 int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnPreviewerStopped;
public PreviewerService(int port)
{
Port = port;
HttpClient = new()
{
BaseAddress = new Uri($"http://localhost:{port}/"),
Timeout = TimeSpan.FromMilliseconds(250)
};
}
public async Task Connect()
{
var isAvailable = await IsPreviewerAvailable();
if (!isAvailable)
{
StartPreviewer();
await WaitForConnection();
}
var previewerVersion = await GetPreviewerVersion();
CheckVersionCompatibility(previewerVersion);
}
private async Task<bool> IsPreviewerAvailable()
{
try
{
var result = await HttpClient.GetAsync("/ping");
return result.IsSuccessStatusCode;
}
catch
{
return false;
}
}
private async Task<Version> GetPreviewerVersion()
{
var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
private void StartPreviewer()
{
try
{
var process = new Process
{
StartInfo = new()
{
FileName = "questpdf-previewer",
Arguments = $"{Port}",
UseShellExecute = false,
CreateNoWindow = true
}
};
process.Start();
Task.Run(async () =>
{
await process.WaitForExitAsync();
OnPreviewerStopped?.Invoke();
});
}
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)
{
if (version.Major == 2022 && version.Minor == 4)
return;
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
$"1) Update the QuestPDF library to newer version. " +
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.4'");
}
private async Task WaitForConnection()
{
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
var isConnected = await IsPreviewerAvailable();
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
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.3.1</Version>
<Version>2022.4.0</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>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -24,7 +24,15 @@
</PropertyGroup>
<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>
+1 -3
View File
@@ -7,9 +7,7 @@
QuestPDF is an open-source .NET library for PDF documents generation.
It offers a layouting engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements.
It offers a layout engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows. All of it done with predictable and discoverable Fluent API.
## Installation
+6 -10
View File
@@ -1,10 +1,6 @@
- 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.
Release theme:
Introduced the QuestPDF Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates its preview every time you make a code change. You don't need to recompile your code after every small adjustment. Save time and enjoy the design process! (available only for dotnet 6 and beyond)
Other changes:
- 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.