Compare commits

..

22 Commits

Author SHA1 Message Date
Marcin Ziąbek 011d44304a Update readme.md 2022-12-14 13:36:56 +01:00
MarcinZiabek a087be8afe Adjusted release notes 2022-12-14 12:33:19 +01:00
MarcinZiabek 483cbb6d46 Nuget packaging: added configuration for readme files 2022-12-13 13:17:53 +01:00
Marcin Ziąbek ec76cbd696 Update dotnet-desktop.yml 2022-12-13 12:26:19 +01:00
Marcin Ziąbek a83d8c43fe Update dotnet-desktop.yml 2022-12-13 12:14:04 +01:00
Marcin Ziąbek 5f25e37f7f Test build script 2022-12-13 12:03:37 +01:00
MarcinZiabek cafd27a60d Upgraded library version, added release notes 2022-12-13 00:08:40 +01:00
MarcinZiabek 5082bbbdf2 Changed letter-spacing to be relative to the font size 2022-12-10 17:34:07 +01:00
MarcinZiabek d2ba21821f Fixed: the Alignment element enforces size constraints in both direction, even if only one is provided 2022-12-09 01:02:47 +01:00
MarcinZiabek 9bef7d64d5 Letter spacing: minor comment improvements 2022-12-06 23:23:19 +01:00
Marcin Ziąbek ca4352addd Merge pull request #398 from Bebo-Maker/letter-spacing
Letter spacing
2022-12-06 23:10:21 +01:00
MarcinZiabek 2906315fa7 Improved obsolete message 2022-12-06 23:05:43 +01:00
Marcin Ziąbek 6f036388b9 Merge pull request #362 from rstm-sf/feature/Text_TextSpanDescriptor
Replace type parameter text for Text
2022-12-06 23:03:44 +01:00
MarcinZiabek 9cd4fd740e Updated nuget dependencies in test projects 2022-12-06 22:51:32 +01:00
MarcinZiabek c97acada76 Upgraded SkiaSharp to 2.88.3 + Avalonia upgrade 2022-12-06 22:49:42 +01:00
Bennet Bo Fenner 19ff9062ad apply letter spacing to each unicode cluster 2022-11-18 15:34:30 +01:00
Bennet Bo Fenner e4f8243c9b add more examples for letter spacing 2022-11-18 15:32:04 +01:00
Bennet Bo Fenner 4345ce857f add example for letter spacing 2022-11-03 16:45:49 +01:00
Bennet Bo Fenner 823572bf25 implement letter spacing 2022-11-03 16:45:40 +01:00
Bennet Bo Fenner e6bd63b3ec handle letter spacing in TextStyleManager 2022-11-03 16:43:49 +01:00
Bennet Bo Fenner bbf3d71e7e add letter spacing to TextStyle 2022-11-03 16:43:29 +01:00
Rustam Sayfutdinov 8a57dceb9d Replace type parameter text for Text 2022-10-08 09:07:03 +03:00
82 changed files with 1731 additions and 926 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Build And Create Nuget Package
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
schedule:
- cron: '3 0 * * 0'
jobs:
main:
runs-on: ${{ matrix.environment }}
strategy:
matrix:
environment:
- macos-latest
- ubuntu-latest
- windows-latest
env:
DOTNET_NOLOGO: 1
DOTNET_CLI_TELEMETRY_OPTOUT: 1
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
UNIT_TEST_PROJECT: QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
steps:
- name: 📝 Fetch Sources 📝
uses: actions/checkout@v2
- name: ⚙ Setup .NET 6.0 SDK ⚙
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- name: 🔄 Restore Nuget Packages 🔄
shell: bash
run: dotnet restore
- name: 🛠 Build Solution 🛠
shell: bash
run: dotnet build -c Release --no-restore
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: Build Package
path: |
**/*.nupkg
**/*.snupkg
!.nuget
+29
View File
@@ -0,0 +1,29 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class AlignmentExamples
{
[Test]
public void Example()
{
RenderingTest
.Create()
.PageSize(400, 200)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.Border(1)
.AlignBottom()
.Background(Colors.Grey.Lighten1)
.Text("Test");
});
}
}
}
@@ -1,4 +1,5 @@
using System.Drawing;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
@@ -43,7 +44,7 @@ namespace QuestPDF.Examples
.Height(50)
.AlignCenter()
.AlignMiddle()
.Text(i)
.Text(i.ToString(CultureInfo.InvariantCulture))
.FontSize(16 + i / 4);
}
});
+3 -2
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
@@ -126,9 +127,9 @@ namespace QuestPDF.Examples
.Padding(5)
.Row(row =>
{
row.ConstantItem(30).Text(index + 1);
row.ConstantItem(30).Text((index + 1).ToString(CultureInfo.InvariantCulture));
row.RelativeItem().Text(item.ItemName);
row.ConstantItem(50).AlignRight().Text(item.Count);
row.ConstantItem(50).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
});
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
@@ -96,9 +97,9 @@ namespace QuestPDF.Examples
{
var item = Items[index];
table.Cell().Element(Style).Text(index + 1);
table.Cell().Element(Style).Text((index + 1).ToString(CultureInfo.InvariantCulture));
table.Cell().Element(Style).Text(item.ItemName);
table.Cell().Element(Style).AlignRight().Text(item.Count);
table.Cell().Element(Style).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -9,7 +9,7 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="Svg.Skia" Version="0.5.18" />
+5 -4
View File
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Drawing;
@@ -389,12 +390,12 @@ namespace QuestPDF.Examples
table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name);
// inches
table.Cell().Element(CellStyle).Text(page.width);
table.Cell().Element(CellStyle).Text(page.height);
table.Cell().Element(CellStyle).Text(page.width.ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text(page.height.ToString(CultureInfo.InvariantCulture));
// points
table.Cell().Element(CellStyle).Text(page.width * inchesToPoints);
table.Cell().Element(CellStyle).Text(page.height * inchesToPoints);
table.Cell().Element(CellStyle).Text((page.width * inchesToPoints).ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text((page.height * inchesToPoints).ToString(CultureInfo.InvariantCulture));
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White);
}
+128 -3
View File
@@ -122,7 +122,132 @@ namespace QuestPDF.Examples
});
});
}
[Test]
public void LetterSpacing()
{
RenderingTest
.Create()
.PageSize(500, 700)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Column(column =>
{
var letterSpacing = new[] { -0.1f, 0f, 0.2f };
var paragraph = Placeholders.Sentence().ToUpper();
foreach (var spacing in letterSpacing)
{
column
.Item()
.Border(1)
.Padding(10)
.Column(nestedColumn =>
{
nestedColumn.Item()
.Text(paragraph)
.FontSize(16)
.LetterSpacing(spacing);
nestedColumn.Item()
.Text($"Letter spacing of {spacing} em")
.FontSize(10)
.Italic()
.FontColor(Colors.Blue.Medium);
});
}
});
});
}
[Test]
public void LetterSpacing_Arabic()
{
RenderingTest
.Create()
.PageSize(500, 700)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(50)
.Column(column =>
{
var letterSpacing = new[] { -0.1f, 0f, 0.2f };
var paragraph = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
foreach (var spacing in letterSpacing)
{
column
.Item()
.Border(1)
.Padding(10)
.Column(nestedColumn =>
{
nestedColumn.Item()
.Text(paragraph)
.FontSize(16)
.FontFamily(Fonts.Calibri)
.LetterSpacing(spacing);
nestedColumn.Item()
.Text($"Letter spacing of {spacing} em")
.FontSize(10)
.Italic()
.FontColor(Colors.Blue.Medium);
});
}
});
});
}
[Test]
public void LetterSpacing_Unicode()
{
RenderingTest
.Create()
.PageSize(500, 700)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(50)
.Column(column =>
{
var letterSpacing = new[] { 0f, 0.5f };
var paragraph = "Ţ̴̡̧̤̮̺̤̗͎̱̹͙͎͖͂̿̓́̉̊̀̍͜h̵̞̘͇̾̎̏̅į̵̹̖͔͉̰̎̉̄̐̏͑͂̅̃̃͘͝s̷͓͉̭̭̯̬̥̻̰̩̦̑̀̀͌́̒̍̒̌̇͛̀͛́̎ ̷̡̡̟͕̳̺̝̼͇͔̬̟̖͍̈́̽͜͝͝i̶͔͚̟̊̐͛́͛̄̌ṡ̸̡̤̪͙͍̥͙̟̼̝̰̥͈̿̓̄̿̓͠ ̶̢̦̙͍̯̖̱̰̯͕͔͎̯̝̎͑t̸͖̲̱̼̎͐̎̉̾̎̾̌̅̔̏͘ȩ̶̝̫̙͓̙̣̔̀̌̔̋̂̑̈́̏̀̈͘̕͜͝s̸̫̝̮̻̼͐̅̄̎̎̑͝ț̷̨̢̨̻͈̮̞̆͗̓͊̃̌͂̑̉̕̕͜͝͝";
foreach (var spacing in letterSpacing)
{
column.Item()
.Text($"Letter spacing of {spacing} em")
.FontSize(10)
.Italic()
.FontColor(Colors.Blue.Medium);
column.Item()
.PaddingVertical(50)
.Text(paragraph)
.FontSize(16)
.FontFamily(Fonts.Calibri)
.LetterSpacing(spacing);
}
});
});
}
[Test]
public void SuperscriptSubscript_Simple()
{
@@ -523,7 +648,7 @@ namespace QuestPDF.Examples
page.Content().Column(column =>
{
column.Item().Text(null);
column.Item().Text((string) null);
column.Item().Text(text =>
{
@@ -555,7 +680,7 @@ namespace QuestPDF.Examples
page.Content().Column(column =>
{
column.Item().Text(null);
column.Item().Text((string) null);
column.Item().Text(text =>
{
+62
View File
@@ -0,0 +1,62 @@
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample;
using QuestPDF.ReportSample.Layouts;
//ImagePlaceholder.Solid = true;
// var model = DataSource.GetReport();
// var report = new StandardReport(model);
// report.ShowInPreviewer();
//
// return;
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hot Reload!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Darken2);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Table(t =>
{
t.ColumnsDefinition(c =>
{
c.RelativeColumn();
c.RelativeColumn(3);
});
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Visual Studio");
t.Cell().Border(1).Padding(5).Text("Start in debug mode with 'Hot Reload on Save' enabled.");
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Command line");
t.Cell().Border(1).Padding(5).Text("Run 'dotnet watch'.");
});
x.Item().Text("Modify this line and the preview should show your changes instantly.");
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.ShowInPreviewer();
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,79 @@
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
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());
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
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();
}
}
+20
View File
@@ -0,0 +1,20 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
class Helpers
{
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
{
using var stream = File.Create(filePath);
using 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();
}
}
}
+202
View File
@@ -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<PreviewPage> 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
}
+6
View File
@@ -0,0 +1,6 @@
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, float Width, float Height);
}
+8
View File
@@ -0,0 +1,8 @@
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="QuestPDF Document Preview">
<Application.Styles>
<FluentTheme Mode="Dark" />
</Application.Styles>
</Application>
+27
View File
@@ -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();
}
}
}
+118
View File
@@ -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<ObservableCollection<PreviewPage>> PagesProperty =
AvaloniaProperty.Register<PreviewerControl, ObservableCollection<PreviewPage>>(nameof(Pages));
public ObservableCollection<PreviewPage>? Pages
{
get => GetValue(PagesProperty);
set => SetValue(PagesProperty, value);
}
public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
public float CurrentScroll
{
get => GetValue(CurrentScrollProperty);
set => SetValue(CurrentScrollProperty, value);
}
public static readonly StyledProperty<float> ScrollViewportSizeProperty = AvaloniaProperty.Register<PreviewerControl, float>(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);
}
}
}
@@ -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; }
}
}
+97
View File
@@ -0,0 +1,97 @@
<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"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
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>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
Pages="{Binding Pages, Mode=OneWay}" />
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
<Button Classes="actions"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
IsVisible="{Binding !!Pages.Count}"
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
</Canvas>
</Viewbox>
</Button>
<Button Classes="actions"
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
ToolTip.Tip="Opens official QuestPDF documentation">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z" />
</Canvas>
</Viewbox>
</Button>
<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">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M12,21.1L10.5,22.4C3.9,16.5 0.5,13.4 0.5,9.6C0.5,8.4 0.9,7.3 1.5,6.4C1.5,6.6 1.5,6.8 1.5,7C1.5,11.7 5.4,15.2 12,21.1M13.6,17C18.3,12.7 21.5,9.9 21.6,7C21.6,5 20.1,3.5 18.1,3.5C16.5,3.5 15,4.5 14.5,5.9H12.6C12,4.5 10.5,3.5 9,3.5C7,3.5 5.5,5 5.5,7C5.5,9.9 8.6,12.7 13.4,17L13.5,17.1M18,1.5C21.1,1.5 23.5,3.9 23.5,7C23.5,10.7 20.1,13.8 13.5,19.8C6.9,13.9 3.5,10.8 3.5,7C3.5,3.9 5.9,1.5 9,1.5C10.7,1.5 12.4,2.3 13.5,3.6C14.6,2.3 16.3,1.5 18,1.5Z" />
</Canvas>
</Viewbox>
</Button>
</StackPanel>
<ScrollBar Grid.Row="1" Grid.Column="1"
Orientation="Vertical"
AllowAutoHide="False"
Minimum="0" Maximum="1"
IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
Value="{Binding CurrentScroll, Mode=TwoWay}"
ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
</Grid>
</Panel>
</Window>
@@ -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);
}
}
}
@@ -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<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> 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<Unit, Unit> ShowPdfCommand { get; }
public ReactiveCommand<Unit, Unit> ShowDocumentationCommand { get; }
public ReactiveCommand<Unit, Unit> SponsorProjectCommand { get; }
public PreviewerWindowViewModel()
{
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.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)
{
using var openBrowserProcess = new Process
{
StartInfo = new()
{
UseShellExecute = true,
FileName = path
}
};
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;
}
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.12.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>
<PackageReadmeFile>PackageReadme.md</PackageReadmeFile>
<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="Resources\Logo.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Logo.png" />
<None Include="Resources\Logo.png" Pack="true" PackagePath="\" />
<None Include="Resources\PackageReadme.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Avalonia" Version="0.10.18" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.18" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.18" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />
<PackageReference Include="ReactiveUI" Version="18.4.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Win32" Version="2.88.3" />
<PackageReference Include="SkiaSharp.NativeAssets.macOS" Version="2.88.3" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
</Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,78 @@
[![Dotnet](https://img.shields.io/badge/platform-.NET-blue)](https://www.nuget.org/packages/QuestPDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/stargazers)
[![Nuget version](https://img.shields.io/nuget/v/QuestPdf)](https://www.nuget.org/packages/QuestPDF/)
[![Nuget download](https://img.shields.io/nuget/dt/QuestPDF)](https://www.nuget.org/packages/QuestPDF/)
[![License](https://img.shields.io/github/license/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
[![Sponsor project](https://img.shields.io/badge/sponsor-project-red)](https://github.com/sponsors/QuestPDF)
QuestPDF is an open-source .NET library for PDF documents generation.
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.
## Documentation
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](https://www.questpdf.com/getting-started.html)
A short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
[![API reference](https://img.shields.io/badge/%F0%9F%93%96%20read-API%20reference-blue)](https://www.questpdf.com/api-reference/index.html)
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/design-patterns.html)
Everything that may help you designing great reports and create reusable code that is easy to maintain.
## Simplicity is the key
How easy it is to start and prototype with QuestPDF? Really easy thanks to its minimal API! Please analyse the code below:
```#
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
// code in your main method
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.GeneratePdf("hello.pdf");
```
And compare it to the produced PDF file:
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/docs/public/minimal-example-shadow.png)
## Are you ready for more?
The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice).
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/docs/public/invoice-small.png)
+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
```
@@ -83,8 +83,6 @@ namespace QuestPDF.ReportSample.Layouts
.Range(0, model.PhotoCount)
.ToList()
.ForEach(x => grid.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>());
//grid.Item().AspectRatio(4 / 3f).Width(100000).Component<ImagePlaceholder>();
});
}
}
+2 -2
View File
@@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample
Content = documentContainer.Compose();
PageContext = new PageContext();
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
var sw = new Stopwatch();
sw.Start();
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample
[Benchmark]
public void GenerationTest()
{
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
}
}
}
@@ -9,7 +9,7 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
+6 -9
View File
@@ -6,7 +6,6 @@ using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample.Layouts;
namespace QuestPDF.ReportSample
@@ -25,13 +24,11 @@ namespace QuestPDF.ReportSample
[Test]
public void GenerateAndShowPdf()
{
Report.ShowInPreviewer();
//ImagePlaceholder.Solid = true;
//
// var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
// Report.GeneratePdf(path);
// Process.Start("explorer.exe", path);
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
Report.GeneratePdf(path);
Process.Start("explorer.exe", path);
}
[Test]
@@ -53,8 +50,8 @@ namespace QuestPDF.ReportSample
var pageContext = new PageContext();
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
}
}
}
+38
View File
@@ -86,5 +86,43 @@ namespace QuestPDF.UnitTests
.ExpectCanvasTranslate(new Position(-300, 0))
.CheckDrawResult();
}
[Test]
public void Draw_HorizontalCenter_VerticalNone()
{
TestPlan
.For(x => new Alignment
{
Horizontal = HorizontalAlignment.Center,
Vertical = null,
Child = x.CreateChild()
})
.DrawElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50)))
.ExpectCanvasTranslate(new Position(150, 0))
.ExpectChildDraw(new Size(100, 300))
.ExpectCanvasTranslate(new Position(-150, 0))
.CheckDrawResult();
}
[Test]
public void Draw_HorizontalNone_VerticalMiddle()
{
TestPlan
.For(x => new Alignment
{
Horizontal = null,
Vertical = VerticalAlignment.Middle,
Child = x.CreateChild()
})
.DrawElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.FullRender(new Size(100, 50)))
.ExpectCanvasTranslate(new Position(0, 125))
.ExpectChildDraw(new Size(400, 50))
.ExpectCanvasTranslate(new Position(0, -125))
.CheckDrawResult();
}
}
}
-54
View File
@@ -1,54 +0,0 @@
using System;
using NUnit.Framework;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.UnitTests
{
public class PreviewerTests
{
[Test]
public void SendError()
{
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
page.Footer()
.AlignCenter()
.Text(x =>
{
//throw new Exception("Dupa!");
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.ShowInPreviewer();
}
}
}
+3 -3
View File
@@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
+11 -5
View File
@@ -8,12 +8,14 @@ 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.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}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleApp", "SimpleApp\SimpleApp.csproj", "{B361D452-80A8-493A-BEA6-AB268E99153E}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -37,9 +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
{B361D452-80A8-493A-BEA6-AB268E99153E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.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
+1 -2
View File
@@ -16,14 +16,13 @@ namespace QuestPDF.Drawing
var container = new Container();
container
.DebugPointer("Document")
.Column(column =>
{
Pages
.SelectMany(x => new List<Action>()
{
() => column.Item().PageBreak(),
() => column.Item().DebugPointer("Page").Component(x)
() => column.Item().Component(x)
})
.Skip(1)
.ToList()
+18 -31
View File
@@ -10,7 +10,6 @@ using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Drawing
{
@@ -52,21 +51,14 @@ namespace QuestPDF.Drawing
return canvas.Images;
}
internal static DocumentPreviewResult GeneratePreviewerPictures(IDocument document)
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
{
var canvas = new SkiaPictureCanvas();
InspectionElement documentHierarchy = null;
RenderDocument(canvas, document, true, x => documentHierarchy = x);
return new DocumentPreviewResult
{
PageSnapshots = canvas.Pictures.ToList(),
DocumentHierarchy = documentHierarchy
};
RenderDocument(canvas, document);
return canvas.Pictures;
}
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, bool applyInspection = false, Action<InspectionElement> inspectionResultHandler = null)
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
where TCanvas : ICanvas, IRenderingCanvas
{
var container = new DocumentContainer();
@@ -75,22 +67,17 @@ namespace QuestPDF.Drawing
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
ApplyContentDirection(content, ContentDirection.LeftToRight);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
if (Settings.EnableCaching)
ApplyCaching(content);
var pageContext = new PageContext();
RenderPass(pageContext, new FreeCanvas(), content);
if (applyInspection)
ApplyInspection(content);
RenderPass(pageContext, canvas, content);
if (applyInspection)
inspectionResultHandler(content.ExtractDocumentHierarchy());
RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
RenderPass(pageContext, canvas, content, debuggingState);
}
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content)
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas
{
InjectDependencies(content, pageContext, canvas);
@@ -103,6 +90,7 @@ namespace QuestPDF.Drawing
while(true)
{
pageContext.SetPageNumber(currentPage);
debuggingState?.Reset();
var spacePlan = content.Measure(Size.Max);
@@ -147,14 +135,9 @@ namespace QuestPDF.Drawing
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
$"Please analyze your documents structure to detect this element and fix its size constraints.";
content.RemoveExistingProxies();
content.RemoveExistingContainers();
content.ApplyLayoutDebugging();
content.Measure(Size.Max);
var layoutTrace = content.CollectLayoutErrorTrace();
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
throw new DocumentLayoutException(message, layoutTrace);
throw new DocumentLayoutException(message, elementTrace);
}
}
@@ -179,12 +162,16 @@ namespace QuestPDF.Drawing
});
}
private static void ApplyInspection(Container content)
private static DebuggingState ApplyDebugging(Container content)
{
var debuggingState = new DebuggingState();
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy or Container ? y : new InspectionProxy(y));
x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
});
return debuggingState;
}
internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
+1 -1
View File
@@ -33,7 +33,7 @@ namespace QuestPDF.Drawing
set => Settings.EnableCaching = value;
}
[Obsolete("The new implementation for debugging layout issues does not introduce additional performance overhead. Therefore, this setting is no longer used since the 2023.1 release. Please remove this setter from your code.")]
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")]
public bool ApplyDebugging
{
get => Settings.EnableDebugging;
@@ -1,13 +1,12 @@
using System;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Exceptions
{
public class DocumentLayoutException : Exception
{
internal LayoutErrorTrace? ElementTrace { get; }
public string? ElementTrace { get; }
internal DocumentLayoutException(string message, LayoutErrorTrace? elementTrace = null) : base(message)
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
{
ElementTrace = elementTrace;
}
+1 -1
View File
@@ -37,7 +37,7 @@ namespace QuestPDF.Drawing.Proxy
base.Draw(availableSpace);
}
private static bool IsClose(float x, float y)
private bool IsClose(float x, float y)
{
return Math.Abs(x - y) < Size.Epsilon;
}
+14
View File
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
public class DebugStackItem
{
public IElement Element { get; internal set; }
public Size AvailableSpace { get; internal set; }
public SpacePlan SpacePlan { get; internal set; }
public ICollection<DebugStackItem> Stack { get; internal set; } = new List<DebugStackItem>();
}
}
+8 -18
View File
@@ -1,34 +1,24 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class DebuggingProxy : ElementProxy
{
internal List<MeasureDetails> Measurements { get; } = new();
private DebuggingState DebuggingState { get; }
public DebuggingProxy(Element child)
public DebuggingProxy(DebuggingState debuggingState, Element child)
{
DebuggingState = debuggingState;
Child = child;
}
internal override SpacePlan Measure(Size availableSpace)
{
var spacePlan = Child.Measure(availableSpace);
Measurements.Add(new MeasureDetails(availableSpace, spacePlan));
DebuggingState.RegisterMeasure(Child, availableSpace);
var spacePlan = base.Measure(availableSpace);
DebuggingState.RegisterMeasureResult(Child, spacePlan);
return spacePlan;
}
}
internal struct MeasureDetails
{
public Size AvailableSpace { get; }
public SpacePlan SpacePlan { get;}
public MeasureDetails(Size availableSpace, SpacePlan spacePlan)
{
AvailableSpace = availableSpace;
SpacePlan = spacePlan;
}
}
}
+130
View File
@@ -0,0 +1,130 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class DebuggingState
{
private DebugStackItem Root { get; set; }
private Stack<DebugStackItem> Stack { get; set; }
public DebuggingState()
{
Reset();
}
public void Reset()
{
Root = null;
Stack = new Stack<DebugStackItem>();
}
public void RegisterMeasure(IElement element, Size availableSpace)
{
if (element.GetType() == typeof(Container))
return;
var item = new DebugStackItem
{
Element = element,
AvailableSpace = availableSpace
};
Root ??= item;
if (Stack.Any())
Stack.Peek().Stack.Add(item);
Stack.Push(item);
}
public void RegisterMeasureResult(IElement element, SpacePlan spacePlan)
{
if (element.GetType() == typeof(Container))
return;
var item = Stack.Pop();
if (item.Element != element)
throw new Exception();
item.SpacePlan = spacePlan;
}
public string BuildTrace()
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(Root);
return builder.ToString();
void Traverse(DebugStackItem item)
{
var indent = new string(' ', nestingLevel * 4);
var title = item.Element.GetType().Name;
if (item.Element is DebugPointer debugPointer)
{
title = debugPointer.Target;
title += debugPointer.Highlight ? " 🌟" : string.Empty;
}
if (item.SpacePlan.Type != SpacePlanType.FullRender)
title = "🔥 " + title;
builder.AppendLine(indent + title);
builder.AppendLine(indent + new string('-', title.Length));
builder.AppendLine(indent + "Available space: " + item.AvailableSpace);
builder.AppendLine(indent + "Requested space: " + item.SpacePlan);
foreach (var configuration in GetElementConfiguration(item.Element))
builder.AppendLine(indent + configuration);
builder.AppendLine();
nestingLevel++;
item.Stack.ToList().ForEach(Traverse);
nestingLevel--;
}
static IEnumerable<string> GetElementConfiguration(IElement element)
{
if (element is DebugPointer)
return Enumerable.Empty<string>();
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
}
}
-45
View File
@@ -1,45 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Proxy;
internal static class Helpers
{
public static IReadOnlyCollection<DocumentElementProperty> GetElementConfiguration(this IElement element)
{
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => new DocumentElementProperty
{
Label = x.Property,
Value = FormatValue(x.Value)
})
.ToList();
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
-36
View File
@@ -1,36 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Drawing.Proxy
{
internal class InspectionProxy : ElementProxy
{
public Dictionary<int, InspectionStateItem> Statistics { get; set; } = new();
public InspectionProxy(Element child)
{
Child = child;
}
internal override void Draw(Size availableSpace)
{
if (Canvas is SkiaCanvasBase canvas)
{
var matrix = canvas.Canvas.TotalMatrix;
var inspectionItem = new InspectionStateItem
{
Element = Child,
Position = new Position(matrix.TransX, matrix.TransY),
Size = availableSpace
};
Statistics[PageContext.CurrentPage] = inspectionItem;
}
base.Draw(availableSpace);
}
}
}
@@ -1,74 +0,0 @@
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Proxy
{
internal static class LayoutErrorAnalyzer
{
public static void ApplyLayoutDebugging(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(y));
});
}
public static void RemoveExistingProxies(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
});
}
public static void RemoveExistingContainers(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is Container container ? container.Child : y);
});
}
public static LayoutErrorTrace CollectLayoutErrorTrace(this Container content)
{
var proxies = content.ExtractProxyOfType<DebuggingProxy>();
return Map(proxies);
LayoutErrorTrace Map(TreeNode<DebuggingProxy> proxy)
{
var element = proxy.Value.Child;
return new LayoutErrorTrace
{
ElementType = element.GetType().Name,
IsSingleChildContainer = element is ContainerElement,
ElementProperties = element.GetElementConfiguration().ToList(),
Measurements = proxy
.Value
.Measurements
.Select(x => new LayoutErrorMeasurement
{
AvailableSpace = new QuestPDF.Previewer.Size()
{
Width = x.AvailableSpace.Width,
Height = x.AvailableSpace.Height
},
SpacePlan = new QuestPDF.Previewer.SpacePlan()
{
Width = x.SpacePlan.Width,
Height = x.SpacePlan.Height,
Type = x.SpacePlan.Type
}
})
.ToList(),
Children = proxy.Children.Select(Map).ToList()
};
}
}
}
}
+15 -2
View File
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using HarfBuzzSharp;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using SkiaSharp;
using Buffer = HarfBuzzSharp.Buffer;
@@ -47,16 +48,28 @@ namespace QuestPDF.Drawing
var xOffset = 0f;
var yOffset = 0f;
var glyphs = new ShapedGlyph[length];
// used for letter spacing calculation
var lastCluster = glyphInfos.LastOrDefault().Cluster;
var letterSpacing = (TextStyle.LetterSpacing ?? 0) * (TextStyle.Size ?? 16);
var glyphs = new ShapedGlyph[length];
for (var i = 0; i < length; i++)
{
// letter spacing should be applied between glyph clusters, not between individual glyphs,
// different cluster id indicates the end of the glyph cluster
if (lastCluster != glyphInfos[i].Cluster)
{
lastCluster = glyphInfos[i].Cluster;
xOffset += letterSpacing;
}
glyphs[i] = new ShapedGlyph
{
Codepoint = (ushort)glyphInfos[i].Codepoint,
Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY),
Width = glyphPositions[i].XAdvance * scaleX
};
};
xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY;
+11 -7
View File
@@ -6,19 +6,23 @@ namespace QuestPDF.Elements
{
internal class Alignment : ContainerElement
{
public VerticalAlignment Vertical { get; set; } = VerticalAlignment.Top;
public HorizontalAlignment Horizontal { get; set; } = HorizontalAlignment.Left;
public VerticalAlignment? Vertical { get; set; }
public HorizontalAlignment? Horizontal { get; set; }
internal override void Draw(Size availableSpace)
{
if (Child == null)
return;
var childSize = base.Measure(availableSpace);
var childMeasurement = base.Measure(availableSpace);
if (childSize.Type == SpacePlanType.Wrap)
if (childMeasurement.Type == SpacePlanType.Wrap)
return;
var childSize = new Size(
Horizontal.HasValue ? childMeasurement.Width : availableSpace.Width,
Vertical.HasValue ? childMeasurement.Height : availableSpace.Height);
var top = GetTopOffset(availableSpace, childSize);
var left = GetLeftOffset(availableSpace, childSize);
@@ -36,7 +40,7 @@ namespace QuestPDF.Elements
VerticalAlignment.Top => 0,
VerticalAlignment.Middle => difference / 2,
VerticalAlignment.Bottom => difference,
_ => throw new NotSupportedException()
_ => 0
};
}
@@ -49,7 +53,7 @@ namespace QuestPDF.Elements
HorizontalAlignment.Left => 0,
HorizontalAlignment.Center => difference / 2,
HorizontalAlignment.Right => difference,
_ => throw new NotSupportedException()
_ => 0
};
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ColumnItem : ContainerElement
internal class ColumnItem : Container
{
public bool IsRendered { get; set; }
}
+1 -2
View File
@@ -1,9 +1,8 @@
using System.ComponentModel;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DebugPointer : ContainerElement
internal class DebugPointer : Container
{
public string Target { get; set; }
public bool Highlight { get; set; }
+1 -6
View File
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class InlinedElement : ContainerElement
internal class InlinedElement : Container
{
}
@@ -49,11 +49,6 @@ namespace QuestPDF.Elements
return Elements;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Elements.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
SetDefaultAlignment();
-5
View File
@@ -20,11 +20,6 @@ namespace QuestPDF.Elements
return Children;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Children.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
return Children
+1 -2
View File
@@ -42,7 +42,6 @@ namespace QuestPDF.Elements
layers
.PrimaryLayer()
.DebugPointer("Page primary layer")
.MinWidth(MinSize.Width)
.MinHeight(MinSize.Height)
@@ -65,9 +64,9 @@ namespace QuestPDF.Elements
decoration
.Content()
.DebugPointer("Page content")
.Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
.Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
.DebugPointer("Page content")
.Element(Content);
decoration
+1 -1
View File
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
Relative
}
internal class RowItem : ContainerElement
internal class RowItem : Container
{
public bool IsRendered { get; set; }
public float Width { get; set; }
-5
View File
@@ -34,11 +34,6 @@ namespace QuestPDF.Elements.Table
return Cells;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Cells.ForEach(x => x.Child = create(x.Child));
}
public void ResetState()
{
Initialize();
+1 -3
View File
@@ -1,8 +1,6 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal class TableCell : ContainerElement, ITableCellContainer
internal class TableCell : Container, ITableCellContainer
{
public int Row { get; set; } = 0;
public int RowSpan { get; set; } = 1;
+2 -2
View File
@@ -60,8 +60,8 @@ namespace QuestPDF.Fluent
{
var descriptor = new GridDescriptor();
if (element is Alignment alignment)
descriptor.Alignment(alignment.Horizontal);
if (element is Alignment alignment && alignment.Horizontal.HasValue)
descriptor.Alignment(alignment.Horizontal.Value);
handler(descriptor);
element.Component(descriptor.Grid);
+10 -4
View File
@@ -276,16 +276,22 @@ namespace QuestPDF.Fluent
descriptor.Compose(element);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
[Obsolete("This method has been deprecated since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
public static void Text(this IContainer element, object? text, TextStyle style)
{
element.Text(text).Style(style);
}
[Obsolete("This method has been deprecated since version 2022.12. Please use an overload where the text parameter is passed explicitly as a string.")]
public static TextSpanDescriptor Text(this IContainer element, object? text)
{
var descriptor = (TextSpanDescriptor) null;
element.Text(x => descriptor = x.Span(text?.ToString()));
return element.Text(text?.ToString());
}
public static TextSpanDescriptor Text(this IContainer element, string? text)
{
var descriptor = (TextSpanDescriptor) null!;
element.Text(x => descriptor = x.Span(text));
return descriptor;
}
}
@@ -55,7 +55,18 @@ namespace QuestPDF.Fluent
descriptor.MutateTextStyle(x => x.LineHeight(value));
return descriptor;
}
/// <summary>
/// Letter spacing controls space between characters. Value 0 corresponds to normal spacing defined by a font.
/// Positive values create additional space, whereas negative values reduce space between characters.
/// Added / reduced space is relative to the font size.
/// </summary>
public static T LetterSpacing<T>(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(x => x.LetterSpacing(value));
return descriptor;
}
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(x => x.Italic(value));
+11 -1
View File
@@ -48,7 +48,17 @@ namespace QuestPDF.Fluent
{
return style.Mutate(TextStyleProperty.LineHeight, value);
}
/// <summary>
/// Letter spacing controls space between characters. Value 0 corresponds to normal spacing defined by a font.
/// Positive values create additional space, whereas negative values reduce space between characters.
/// Added / reduced space is relative to the font size.
/// </summary>
public static TextStyle LetterSpacing(this TextStyle style, float value)
{
return style.Mutate(TextStyleProperty.LetterSpacing, value);
}
public static TextStyle Italic(this TextStyle style, bool value = true)
{
return style.Mutate(TextStyleProperty.IsItalic, value);
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Infrastructure
}
class Slot : ContainerElement, ISlot
class Slot : Container, ISlot
{
}
+2
View File
@@ -10,6 +10,7 @@ namespace QuestPDF.Infrastructure
internal string? FontFamily { get; set; }
internal float? Size { get; set; }
internal float? LineHeight { get; set; }
internal float? LetterSpacing { get; set; }
internal FontWeight? FontWeight { get; set; }
internal FontPosition? FontPosition { get; set; }
internal bool? IsItalic { get; set; }
@@ -27,6 +28,7 @@ namespace QuestPDF.Infrastructure
FontFamily = Fonts.Lato,
Size = 12,
LineHeight = 1.2f,
LetterSpacing = 0,
FontWeight = Infrastructure.FontWeight.Normal,
FontPosition = Infrastructure.FontPosition.Normal,
IsItalic = false,
@@ -11,6 +11,7 @@ namespace QuestPDF.Infrastructure
FontFamily,
Size,
LineHeight,
LetterSpacing,
FontWeight,
FontPosition,
IsItalic,
@@ -102,6 +103,19 @@ namespace QuestPDF.Infrastructure
return origin with { LineHeight = castedValue };
}
if(property == TextStyleProperty.LetterSpacing)
{
if (!overrideValue && origin.LetterSpacing != null)
return origin;
var castedValue = (float?)value;
if (origin.LetterSpacing == castedValue)
return origin;
return origin with { LetterSpacing = castedValue };
}
if (property == TextStyleProperty.FontWeight)
{
@@ -246,6 +260,7 @@ namespace QuestPDF.Infrastructure
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.LetterSpacing, parent.LetterSpacing, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
-44
View File
@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Previewer;
internal sealed class IncompatibleVersionApiRequest
{
}
internal sealed class NotifyPresenceApiRequest
{
public string ClientId { get; set; }
public string LibraryVersion { get; set; }
public bool IsDotnet6OrBeyond { get; set; }
public bool IsDotnet3OrBeyond { get; set; }
public bool IsExecutedInUnitTest { get; set; }
}
internal sealed class DocumentPreviewApiRequest
{
public ICollection<PageSnapshot> PageSnapshots { get; set; }
public InspectionElement DocumentHierarchy { get; set; }
public class PageSnapshot
{
public string ResourceId { get; } = Guid.NewGuid().ToString("N");
public int PageNumber { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
}
internal sealed class GenericErrorApiRequest
{
public GenericError? Error { get; set; }
}
internal sealed class LayoutErrorApiRequest
{
public LayoutErrorTrace? Trace { get; set; }
}
+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;
}
});
});
}
}
}
-22
View File
@@ -1,22 +0,0 @@
#if NETCOREAPP3_0_OR_GREATER
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace QuestPDF.Previewer;
internal static class Helpers
{
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(value, options);
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
return client.PostAsync(requestUri, jsonContent, cancellationToken);
}
}
#endif
@@ -1,60 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer.Inspection;
public static class DocumentHierarchyProcessor
{
internal static InspectionElement ExtractDocumentHierarchy(this Container content)
{
content.RemoveExistingContainers();
var proxies = content.ExtractProxyOfType<InspectionProxy>();
return Map(proxies);
InspectionElement Map(TreeNode<InspectionProxy> treeNode)
{
var proxy = treeNode.Value;
var element = proxy.Child;
var locations = proxy
.Statistics
.Keys
.Select(x =>
{
var statistics = proxy.Statistics[x];
return new InspectionElementLocation
{
PageNumber = x,
Position = new MyPosition
{
Top = statistics.Position.Y,
Left = statistics.Position.X
},
Size = new Size
{
Width = statistics.Size.Width,
Height = statistics.Size.Height
}
};
})
.ToList();
return new InspectionElement
{
ElementType = element.GetType().Name,
IsSingleChildContainer = element is ContainerElement,
Location = locations,
Properties = element.GetElementConfiguration().ToList(),
Children = treeNode.Children.Select(Map).ToList()
};
}
}
}
@@ -1,10 +0,0 @@
using System.Collections.Generic;
using QuestPDF.Drawing;
namespace QuestPDF.Previewer.Inspection;
internal class DocumentPreviewResult
{
public IList<PreviewerPicture> PageSnapshots { get; set; }
public InspectionElement DocumentHierarchy { get; set; }
}
@@ -1,34 +0,0 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer.Inspection
{
internal class InspectionStateItem
{
public Element Element { get; internal set; }
public Infrastructure.Size Size { get; internal set; }
public Position Position { get; internal set; }
}
internal class MyPosition
{
public float Top { get; set; }
public float Left { get; set; }
}
internal class InspectionElementLocation
{
public int PageNumber { get; internal set; }
public MyPosition Position { get; internal set; }
public Size Size { get; internal set; }
}
internal class InspectionElement
{
public string ElementType { get; set; }
public bool IsSingleChildContainer { get; set; }
public IReadOnlyCollection<InspectionElementLocation> Location { get; set; }
public IReadOnlyCollection<DocumentElementProperty> Properties { get; set; }
public ICollection<InspectionElement> Children { get; set; }
}
}
-63
View File
@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Previewer;
internal sealed class DocumentElementProperty
{
public string Label { get; set; }
public string Value { get; set; }
}
internal sealed class Size
{
public float Width { get; set; }
public float Height { get; set; }
}
internal sealed class SpacePlan
{
public float Width { get; set; }
public float Height { get; set; }
public SpacePlanType Type { get; set; }
}
internal sealed class LayoutErrorTrace
{
public string ElementType { get; set; }
public bool IsSingleChildContainer { get; set; }
public IReadOnlyCollection<DocumentElementProperty> ElementProperties { get; set; }
public IReadOnlyCollection<LayoutErrorMeasurement> Measurements { get; set; }
public IReadOnlyCollection<LayoutErrorTrace> Children { get; set; }
}
internal sealed class LayoutErrorMeasurement
{
public Size AvailableSpace { get; set; }
public SpacePlan SpacePlan { get; set; }
}
internal sealed class GenericError
{
public string ExceptionType { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
public GenericError? InnerException { get; set; }
}
+28 -25
View File
@@ -1,11 +1,10 @@
#if NETCOREAPP3_0_OR_GREATER
#if NET6_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
@@ -17,43 +16,47 @@ namespace QuestPDF.Previewer
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
}
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
{
var previewerService = new PreviewerService(port);
using var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnLostConnection += () => cancellationTokenSource.Cancel();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
await RefreshPreview();
#if NET6_0_OR_GREATER
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
#endif
while (true)
{
if (cancellationToken.IsCancellationRequested)
break;
// ReSharper disable once MethodSupportsCancellation
await Task.Delay(TimeSpan.FromMilliseconds(1000));
}
await WaitForPreviewerExit(cancellationTokenSource.Token);
Task RefreshPreview()
{
try
var pictures = GetPictures();
return previewerService.RefreshPreview(pictures);
ICollection<PreviewerPicture> GetPictures()
{
var documentPreviewResult = DocumentGenerator.GeneratePreviewerPictures(document);
return previewerService.ShowDocumentPreview(documentPreviewResult);
try
{
return DocumentGenerator.GeneratePreviewerPictures(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
}
}
catch (DocumentLayoutException exception)
}
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
{
while (true)
{
return previewerService.ShowLayoutError(exception);
}
catch (Exception exception)
{
return previewerService.ShowGenericError(exception);
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}
}
}
@@ -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
+53 -156
View File
@@ -1,32 +1,25 @@
#if NETCOREAPP3_0_OR_GREATER
#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.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Previewer.Inspection;
using QuestPDF.Drawing;
namespace QuestPDF.Previewer
{
internal class PreviewerService
{
private string ClientId { get; } = Guid.NewGuid().ToString("D");
private string LibraryVersion { get; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString();
private int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnLostConnection;
private const int RequiredPreviewerVersionMajor = 2023;
private const int RequiredPreviewerVersionMinor = 1;
public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 12;
public PreviewerService(int port)
{
@@ -37,23 +30,11 @@ namespace QuestPDF.Previewer
Timeout = TimeSpan.FromSeconds(1)
};
}
internal static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
MaxDepth = 256,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
#region Connection
public async Task Connect()
{
var isAvailable = await IsPreviewerAvailable();
if (!isAvailable)
{
StartPreviewer();
@@ -61,11 +42,9 @@ namespace QuestPDF.Previewer
}
var previewerVersion = await GetPreviewerVersion();
await CheckVersionCompatibility(previewerVersion);
StartNotifyPresenceTask();
CheckVersionCompatibility(previewerVersion);
}
private async Task<bool> IsPreviewerAvailable()
{
try
@@ -79,6 +58,12 @@ namespace QuestPDF.Previewer
}
}
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
private void StartPreviewer()
{
try
@@ -95,6 +80,13 @@ namespace QuestPDF.Previewer
};
process.Start();
Task.Run(async () =>
{
await process.WaitForExitAsync();
process.Dispose();
OnPreviewerStopped?.Invoke();
});
}
catch
{
@@ -102,6 +94,16 @@ namespace QuestPDF.Previewer
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
}
}
private void CheckVersionCompatibility(Version version)
{
if (version.Major == RequiredPreviewerVersionMajor && version.Minor == RequiredPreviewerVersionMinor)
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 {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task WaitForConnection()
{
@@ -112,155 +114,50 @@ namespace QuestPDF.Previewer
while (true)
{
// ReSharper disable once MethodSupportsCancellation
await Task.Delay(TimeSpan.FromMilliseconds(250));
if (cancellationToken.IsCancellationRequested)
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
var isConnected = await IsPreviewerAvailable();
if (isConnected)
break;
}
}
#endregion
#region Checking compatibility
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
var response = await result.Content.ReadFromJsonAsync<string>();
return Version.Parse(response);
}
private async Task CheckVersionCompatibility(Version version)
{
if (version is { Major: RequiredPreviewerVersionMajor, Minor: RequiredPreviewerVersionMinor })
return;
await ShowIncompatibleVersion();
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 {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task ShowIncompatibleVersion()
{
var payload = new IncompatibleVersionApiRequest();
await HttpClient.PostAsJsonAsync("/update/incompatibleVersion", payload, JsonSerializerOptions);
}
#endregion
#region Reporting presence
private void StartNotifyPresenceTask()
{
Task.Run(async () =>
{
while (true)
{
await NotifyPresence();
await Task.Delay(1000);
}
});
}
private async Task NotifyPresence()
{
var payload = new NotifyPresenceApiRequest
{
ClientId = ClientId,
LibraryVersion = LibraryVersion,
IsDotnet6OrBeyond = RuntimeDetector.IsNet6OrGreater,
IsDotnet3OrBeyond = RuntimeDetector.IsNet3OrGreater,
IsExecutedInUnitTest = UnitTestDetector.RunningInUnitTest
};
await HttpClient.PostAsJsonAsync("/update/notifyPresence", payload);
}
#endregion
#region Updating state
public async Task ShowDocumentPreview(DocumentPreviewResult documentPreviewResult)
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
{
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
var pages = new List<DocumentPreviewApiRequest.PageSnapshot>();
foreach (var snapshot in documentPreviewResult.PageSnapshots)
foreach (var picture in pictures)
{
var page = new DocumentPreviewApiRequest.PageSnapshot
var page = new PreviewerRefreshCommand.Page
{
PageNumber = documentPreviewResult.PageSnapshots.IndexOf(snapshot) + 1,
Width = snapshot.Size.Width,
Height = snapshot.Size.Height
Width = picture.Size.Width,
Height = picture.Size.Height
};
pages.Add(page);
var pictureStream = snapshot.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), "snapshots", page.ResourceId);
var pictureStream = picture.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
}
var request = new DocumentPreviewApiRequest
var command = new PreviewerRefreshCommand
{
PageSnapshots = pages,
DocumentHierarchy = documentPreviewResult.DocumentHierarchy
Pages = pages
};
var json = JsonSerializer.Serialize(request, JsonSerializerOptions);
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
multipartContent.Add(jsonContent, "request");
using var response = await HttpClient.PostAsync("/update/documentPreview", multipartContent);
response.EnsureSuccessStatusCode();
foreach (var picture in documentPreviewResult.PageSnapshots)
multipartContent.Add(JsonContent.Create(command), "command");
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
foreach (var picture in pictures)
picture.Picture.Dispose();
}
public async Task ShowGenericError(Exception exception)
{
var payload = new GenericErrorApiRequest
{
Error = MapException(exception)
};
await HttpClient.PostAsJsonAsync("/update/genericError", payload, JsonSerializerOptions);
static GenericError? MapException(Exception? exception)
{
if (exception == null)
return null;
return new GenericError
{
ExceptionType = exception.GetType().FullName,
Message = exception.Message,
StackTrace = exception.StackTrace,
InnerException = MapException(exception.InnerException)
};
}
}
public async Task ShowLayoutError(DocumentLayoutException exception)
{
var payload = new LayoutErrorApiRequest
{
Trace = exception.ElementTrace
};
await HttpClient.PostAsJsonAsync("/update/layoutError", payload, JsonSerializerOptions);
}
#endregion
}
}
-16
View File
@@ -1,16 +0,0 @@
namespace QuestPDF.Previewer;
public static class RuntimeDetector
{
#if NET6_0_OR_GREATER
public static bool IsNet6OrGreater => true;
#else
public static bool IsNet6OrGreater => false;
#endif
#if NETCOREAPP3_0_OR_GREATER
public static bool IsNet3OrGreater => true;
#else
public static bool IsNet6OrGreater => false;
#endif
}
-38
View File
@@ -1,38 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer;
internal record TreeNode<T>(T Value, ICollection<TreeNode<T>> Children);
internal static class TreeTraversal
{
public static TreeNode<T> ExtractProxyOfType<T>(this Element root) where T : ElementProxy
{
return Traverse(root).FirstOrDefault();
IEnumerable<TreeNode<T>> Traverse(Element element)
{
if (element is T proxy)
{
var result = new TreeNode<T>(proxy, new List<TreeNode<T>>());
proxy
.Child!
.GetChildren()
.SelectMany(Traverse)
.ToList()
.ForEach(result.Children.Add);
yield return result;
}
else
{
foreach (var treeNode in element.GetChildren().SelectMany(Traverse))
yield return treeNode;
}
}
}
}
-20
View File
@@ -1,20 +0,0 @@
using System;
using System.Linq;
namespace QuestPDF.Previewer;
internal static class UnitTestDetector
{
public static bool RunningInUnitTest { get; }
private static string[] UnitTestAssemblies = { "xunit", "nunit", "mstest" };
static UnitTestDetector()
{
RunningInUnitTest = AppDomain
.CurrentDomain
.GetAssemblies()
.Select(x => x.FullName.ToLowerInvariant())
.Any(x => UnitTestAssemblies.Any(x.Contains));
}
}
+8 -12
View File
@@ -3,23 +3,24 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.11.0</Version>
<Version>2022.12.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>10</LangVersion>
<LangVersion>9</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon>
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
<PackageReadmeFile>PackageReadme.md</PackageReadmeFile>
<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</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Nullable>enable</Nullable>
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
@@ -39,11 +40,10 @@
<EmbeddedResource Include="Resources\ImagePlaceholder.png" />
<EmbeddedResource Include="Resources\Logo.png" />
<None Remove="ImagePlaceholder.png" />
<None Include="Resources\Logo.png">
<Pack>true</Pack>
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
<None Include="Resources\Logo.png" Pack="true" PackagePath="\" />
<None Include="Resources\PackageReadme.md" Pack="true" PackagePath="\" />
<None Remove="Resources\DefaultFont\Lato-Black.ttf" />
<EmbeddedResource Include="Resources\DefaultFont\Lato-Black.ttf" />
<None Remove="Resources\DefaultFont\Lato-BlackItalic.ttf" />
@@ -65,8 +65,4 @@
<None Remove="Resources\DefaultFont\Lato-ThinItalic.ttf" />
<EmbeddedResource Include="Resources\DefaultFont\Lato-ThinItalic.ttf" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Previewer\Models\GenericError.cs" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -21,7 +21,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.11.0" />
<PackageReference Include="QuestPDF" Version="2022.12.0" />
```
## Documentation
+4 -3
View File
@@ -1,3 +1,4 @@
Added support for the right-to-left content direction.
Fixed: word-wrapping algorithm does not work correctly with right-to-left languages.
Fixed: Page.Size() API incorrectly uses Unit.Inch as default unit. Replaced with Unit.Point for consistency.
Feature: implemented LetterSpacing property for the Text element
Improvement: the Text element API accepts now only string values, objects are not automatically converted anymore
Fix: the Alignment element incorrectly limits size of its child when only one axis is set (horizontal or vertical)
Maintenance: Updated SkiaSharp dependency to 2.88.3
+2 -5
View File
@@ -1,6 +1,4 @@
using System;
namespace QuestPDF
namespace QuestPDF
{
public static class Settings
{
@@ -26,9 +24,8 @@ namespace QuestPDF
/// It includes layout calculation results and path to the problematic area.
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
[Obsolete("The new implementation for debugging layout issues does not introduce any additional performance overhead. Therefore, this setting is no longer used since the 2023.1 release. Please remove this setter from your code.")]
public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
/// <summary>
/// This flag enables checking the font glyph availability.
/// If your text contains glyphs that are not present in the specified font,
-56
View File
@@ -1,56 +0,0 @@
// See https://aka.ms/new-console-template for more information
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
QuestPDF.Settings.EnableCaching = true;
QuestPDF.Settings.EnableDebugging = true;
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
foreach (var i in Enumerable.Range(0, 50))
{
x.Item()
.Height(80).Width(100 + i * 5)
.Background(Colors.Grey.Lighten3)
.AlignCenter().AlignMiddle()
.Text(i);
}
//x.Item().Width(1000);
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.ShowInPreviewer();
-15
View File
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -23,7 +23,7 @@ Choosing a project dependency could be difficult. We need to ensure stability an
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
<img src="https://user-images.githubusercontent.com/9263853/200136456-95b7a1e2-0b7e-4779-9071-9d4f2ef50745.png" width="709" />
<img src="https://user-images.githubusercontent.com/9263853/207596765-f3b84375-e4da-4597-b6a4-f2e22dd2b479.png" width="800" />
## Please share with the community
@@ -63,7 +63,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.11.0" />
<PackageReference Include="QuestPDF" Version="2022.12.0" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)