Compare commits

..

2 Commits

Author SHA1 Message Date
MarcinZiabek bd577b9609 Implemented custom tree view with proper indendation 2022-07-18 10:47:07 +02:00
MarcinZiabek 100afbdc32 Prototype implementation 2022-07-12 15:21:50 +02:00
331 changed files with 2663 additions and 4640 deletions
-54
View File
@@ -1,54 +0,0 @@
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
working-directory: ./Source
- name: 🛠 Build Solution 🛠
shell: bash
run: dotnet build -c Release --no-restore
working-directory: ./Source
- name: Upload build artifacts
uses: actions/upload-artifact@v3
if: ${{ matrix.environment == 'windows-latest' }}
with:
name: Build Package
path: |
**/*.nupkg
**/*.snupkg
!.nuget
View File
@@ -1,5 +1,4 @@
using System.Drawing;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
@@ -44,7 +43,7 @@ namespace QuestPDF.Examples
.Height(50)
.AlignCenter()
.AlignMiddle()
.Text(i.ToString(CultureInfo.InvariantCulture))
.Text(i)
.FontSize(16 + i / 4);
}
});
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
@@ -50,15 +49,15 @@ namespace QuestPDF.Examples
{
container.MinimalBox().Decoration(decoration =>
{
decoration.Before().Element(header);
decoration.Header().Element(header);
decoration.Content().Column(column =>
decoration.Content().Box().Stack(stack =>
{
foreach (var row in rows)
column.Item().Element(row.Element);
stack.Item().Element(row.Element);
});
decoration.After().Element(footer);
decoration.Footer().Element(footer);
});
});
@@ -83,14 +82,15 @@ namespace QuestPDF.Examples
.BorderBottom(1)
.BorderColor(Colors.Grey.Darken2)
.Padding(5)
.DefaultTextStyle(TextStyle.Default.SemiBold())
.Row(row =>
{
row.ConstantItem(30).Text("#");
row.RelativeItem().Text("Item name");
row.ConstantItem(50).AlignRight().Text("Count");
row.ConstantItem(50).AlignRight().Text("Price");
row.ConstantItem(50).AlignRight().Text("Total");
var textStyle = TextStyle.Default.SemiBold();
row.ConstantItem(30).Text("#", textStyle);
row.RelativeItem().Text("Item name", textStyle);
row.ConstantItem(50).AlignRight().Text("Count", textStyle);
row.ConstantItem(50).AlignRight().Text("Price", textStyle);
row.ConstantItem(50).AlignRight().Text("Total", textStyle);
});
});
}
@@ -105,8 +105,7 @@ namespace QuestPDF.Examples
.Width(context.AvailableSize.Width)
.Padding(5)
.AlignRight()
.DefaultTextStyle(TextStyle.Default.FontSize(14).SemiBold())
.Text($"Subtotal: {total}$");
.Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
});
}
@@ -127,9 +126,9 @@ namespace QuestPDF.Examples
.Padding(5)
.Row(row =>
{
row.ConstantItem(30).Text((index + 1).ToString(CultureInfo.InvariantCulture));
row.ConstantItem(30).Text(index + 1);
row.RelativeItem().Text(item.ItemName);
row.ConstantItem(50).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
row.ConstantItem(50).AlignRight().Text(item.Count);
row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
});
@@ -165,7 +164,7 @@ namespace QuestPDF.Examples
.Decoration(decoration =>
{
decoration
.Before()
.Header()
.PaddingBottom(5)
.Text(text =>
{
@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
@@ -97,9 +96,9 @@ namespace QuestPDF.Examples
{
var item = Items[index];
table.Cell().Element(Style).Text((index + 1).ToString(CultureInfo.InvariantCulture));
table.Cell().Element(Style).Text(index + 1);
table.Cell().Element(Style).Text(item.ItemName);
table.Cell().Element(Style).AlignRight().Text(item.Count.ToString(CultureInfo.InvariantCulture));
table.Cell().Element(Style).AlignRight().Text(item.Count);
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
@@ -1,12 +1,9 @@
using System;
using System.IO;
using System.Linq;
using System.IO;
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
@@ -37,21 +34,6 @@ namespace QuestPDF.Examples
});
}
[Test]
public void DynamicImage()
{
RenderingTest
.Create()
.PageSize(450, 350)
.ProducePdf()
.ShowResults()
.Render(page =>
{
page.Padding(25)
.Image(Placeholders.Image);
});
}
[Test]
public void Exception()
{
@@ -65,25 +47,5 @@ namespace QuestPDF.Examples
.Render(page => page.Image("non_existent.png"));
});
}
[Test]
public void ImageResolutionScaling()
{
var image = Image.FromFile("large-image.jpg");
Document
.Create(document =>
{
document.Page(page =>
{
page.Size(210, 210);
page.Margin(50);
page.Content().Image(image);
});
})
.GeneratePdf($"test.pdf");
}
}
}
@@ -120,42 +120,5 @@ namespace QuestPDF.Examples
.Background(Placeholders.BackgroundColor());
}
}
[Test]
public void RepeatingInlinedInHeader_Test()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(document =>
{
document.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(1, Unit.Inch);
page.PageColor(Colors.White);
page.Header()
.Inlined(inlined =>
{
inlined.Spacing(10);
foreach (var i in Enumerable.Range(5, 5))
inlined.Item().Width(i * 10).Height(20).Background(Colors.Red.Medium);
});
page.Content()
.PaddingVertical(20)
.Column(column =>
{
column.Spacing(25);
foreach (var i in Enumerable.Range(10, 20))
column.Item().Width(i * 10).Height(50).Background(Colors.Grey.Lighten2);
});
});
});
}
}
}
@@ -19,12 +19,10 @@ namespace QuestPDF.Examples
{
var url = "https://picsum.photos/300/200";
if (Greyscale)
if(Greyscale)
url += "?grayscale";
using var client = new WebClient();
client.Headers.Add("user-agent", "QuestPDF/1.0 Unit Testing");
var response = client.DownloadData(url);
container.Image(response);
}
@@ -61,4 +59,4 @@ namespace QuestPDF.Examples
});
}
}
}
}
@@ -6,13 +6,13 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.3" />
<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" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
</ItemGroup>
<ItemGroup>
@@ -32,12 +32,6 @@
<None Update="pdf-icon.svg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="large-image.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="large-image.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -105,41 +105,5 @@ namespace QuestPDF.Examples
.Row(row => { });
});
}
[Test]
public void RowElementForRelativeHeightDivision()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.MaxPages(100)
.PageSize(250, 400)
.Render(container =>
{
container
.Padding(25)
.AlignLeft()
.RotateRight()
.Row(row =>
{
row.Spacing(20);
row.RelativeItem(1).Element(Content);
row.RelativeItem(2).Element(Content);
row.RelativeItem(3).Element(Content);
void Content(IContainer container)
{
container
.RotateLeft()
.Border(1)
.Background(Placeholders.BackgroundColor())
.Padding(5)
.Text(Placeholders.Label());
}
});
});
}
}
}
@@ -3,7 +3,6 @@ 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;
@@ -300,18 +299,17 @@ namespace QuestPDF.Examples
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
var image = Placeholders.Image(400, 300);
table.Cell().RowSpan(4).Element(Block).Text("1");
table.Cell().Image(image);
table.Cell().Image(image);
table.Cell().Image(image);
table.Cell().Image(image);
table.Cell().Image(image);
table.Cell().Image(image);
table.Cell().RowSpan(2).Element(Block).Text("2");
table.Cell().RowSpan(1).Element(Block).Text("3");
table.Cell().RowSpan(1).Element(Block).Text("4");
table.Cell().RowSpan(2).Element(Block).Text("5");
table.Cell().RowSpan(1).Element(Block).Text("6");
table.Cell().RowSpan(1).Element(Block).Text("7");
});
});
}
@@ -391,12 +389,12 @@ namespace QuestPDF.Examples
table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name);
// inches
table.Cell().Element(CellStyle).Text(page.width.ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text(page.height.ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text(page.width);
table.Cell().Element(CellStyle).Text(page.height);
// points
table.Cell().Element(CellStyle).Text((page.width * inchesToPoints).ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text((page.height * inchesToPoints).ToString(CultureInfo.InvariantCulture));
table.Cell().Element(CellStyle).Text(page.width * inchesToPoints);
table.Cell().Element(CellStyle).Text(page.height * inchesToPoints);
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White);
}
@@ -111,7 +111,7 @@ namespace QuestPDF.Examples
{
page.Margin(50);
page.Content().PaddingVertical(10).Column(column =>
page.Content().Column(column =>
{
column.Item().Element(Title);
column.Item().PageBreak();
@@ -1,13 +1,11 @@
using System;
using System;
using System.Linq;
using System.Text;
using NUnit.Framework;
using QuestPDF.Elements.Text;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Examples
{
@@ -123,132 +121,7 @@ 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()
{
@@ -649,7 +522,7 @@ namespace QuestPDF.Examples
page.Content().Column(column =>
{
column.Item().Text((string) null);
column.Item().Text(null);
column.Item().Text(text =>
{
@@ -681,7 +554,7 @@ namespace QuestPDF.Examples
page.Content().Column(column =>
{
column.Item().Text((string) null);
column.Item().Text(null);
column.Item().Text(text =>
{
@@ -717,7 +590,7 @@ namespace QuestPDF.Examples
text.Span("Complex Unicode structure: ");
text.Span("T̶̖̔͆͆̽̔ḩ̷̼̫̐̈́̀͜͝͝ì̶͇̤͓̱̣͇͓͉̎s̵̡̟̹͍̜͉̗̾͛̈̐́͋͂͝͠ͅ ̴̨͙͍͇̭̒͗̀́͝ì̷̡̺͉̼̏̏̉̌͝s̷͍͙̗̰̖͙̈̑̂̔͑͊̌̓̊̇͜ ̶̛̼͚͊̅͘ṭ̷̨̘̣̙̖͉͌̏̂̅͑̄̽̕͝ȅ̶̲̲̙̭͈̬̣͔̝͔̈́͝s̸̢̯̪̫͓̭̮̓̀͆͜ț̸̢͉̞̥̤̏̌̓͝").FontFamily(Fonts.Calibri).FontColor(Colors.Red.Medium);
text.Span("T̶̖̔͆͆̽̔ḩ̷̼̫̐̈́̀͜͝͝ì̶͇̤͓̱̣͇͓͉̎s̵̡̟̹͍̜͉̗̾͛̈̐́͋͂͝͠ͅ ̴̨͙͍͇̭̒͗̀́͝ì̷̡̺͉̼̏̏̉̌͝s̷͍͙̗̰̖͙̈̑̂̔͑͊̌̓̊̇͜ ̶̛̼͚͊̅͘ṭ̷̨̘̣̙̖͉͌̏̂̅͑̄̽̕͝ȅ̶̲̲̙̭͈̬̣͔̝͔̈́͝s̸̢̯̪̫͓̭̮̓̀͆͜ț̸̢͉̞̥̤̏̌̓͝").FontColor(Colors.Red.Medium);
text.Span(".");
@@ -730,7 +603,7 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.PageSize(250, 100)
.PageSize(500, 100)
.ProduceImages()
.ShowResults()
@@ -740,268 +613,8 @@ namespace QuestPDF.Examples
.Padding(25)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text("خوارزمية ترتيب")
.FontFamily(Fonts.Calibri)
.FontSize(30);
});
}
[Test]
public void FontFallback()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x
.Fallback(y => y.FontFamily("Segoe UI Emoji")
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
page.Size(PageSizes.A4);
page.Content().Text(t =>
{
t.Line("This is normal text.");
t.EmptyLine();
t.Line("Following line should use font fallback:");
t.Line("中文文本");
t.EmptyLine();
t.Line("The following line contains a mix of known and unknown characters.");
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
t.EmptyLine();
t.Line("Emojis work out of the box because of font fallback: 😊😅🥳👍❤😍👌");
});
});
});
}
[Test]
public void WordWrappingStability()
{
// instruction: check if any characters repeat when performing the word-wrapping algorithm
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProducePdf()
.ShowResults()
.Render(container =>
{
var text = "Lorem ipsum dolor sit amet consectetuer";
container
.Padding(20)
.Column(column =>
{
column.Spacing(10);
foreach (var width in Enumerable.Range(25, 200))
{
column
.Item()
.MaxWidth(width)
.Background(Colors.Grey.Lighten3)
.Text(text);
}
});
});
}
[Test]
public void AdvancedLanguagesSupport()
{
RenderingTest
.Create()
.PageSize(new PageSize(400, 400))
.ProduceImages()
.ShowResults()
.Render(container =>
{
var text = "في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد.";
container
.Padding(20)
.ContentFromRightToLeft()
.Text(text)
.FontFamily(Fonts.Calibri)
.FontSize(22);
});
}
[Test]
public void WordWrappingWhenRightToLeft()
{
RenderingTest
.Create()
.PageSize(new PageSize(1000, 500))
.ProducePdf()
.ShowResults()
.Render(container =>
{
var text = "في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد.";
container
.Padding(25)
.ContentFromRightToLeft()
.Column(column =>
{
column.Spacing(20);
foreach (var size in new[] { 36, 34, 32, 30, 15 })
{
column
.Item()
.ShowEntire()
.MaxWidth(size * 25)
.Background(Colors.Grey.Lighten3)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text(text)
.FontSize(20)
.FontFamily("Segoe UI");
}
});
});
}
[Test]
public void ForcingTextDirection()
{
RenderingTest
.Create()
.PageSize(new PageSize(1000, 500))
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.DefaultTextStyle(x => x.FontSize(24).FontFamily("Calibri"))
.Column(column =>
{
column.Spacing(10);
var word = "الجوريتم";
var definition = "algorithm in Arabic";
var text = $"{word} - {definition}";
// text direction is automatically detected using the first word
column.Item().Text(text);
// it is possible to force specific content direction
column.Item().Text(text).DirectionFromLeftToRight();
column.Item().Text(text).DirectionFromRightToLeft();
// to combine text in various content directions, split it into segments
column.Item().Text(text =>
{
text.Span(word);
text.Span(" - ");
text.Span(definition);
});
});
});
}
[Test]
public void DetectSpanPositionExample()
{
RenderingTest
.Create()
.PageSize(new PageSize(650, 800))
.ProduceImages()
.ShowResults()
.Render(container =>
{
var fontSize = 20;
var paint = new SKPaint
{
Color = SKColors.Red,
TextSize = fontSize
};
var fontMetrics = paint.FontMetrics;
var start = 0f;
var end = 0f;
// corner case: what if text is paged? clamp start and end?
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(fontSize).FontFamily("Calibri"))
.Layers(layers =>
{
layers.PrimaryLayer().Text(text =>
{
text.Span(Placeholders.Paragraph());
text.Span(" - ");
// record start
text.Element().Width(0).Height(0)
.Canvas((canvas, size) => start = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
text.Span(Placeholders.LoremIpsum()).BackgroundColor(Colors.Red.Lighten4);
// record end
text.Element().Width(0).Height(0)
.Canvas((canvas, size) => end = canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
text.Span(" - ");
text.Span(Placeholders.Paragraph());
});
layers.Layer().Canvas((canvas, size) =>
{
canvas.Save();
canvas.Translate(-canvas.TotalMatrix.TransX / canvas.TotalMatrix.ScaleX, -canvas.TotalMatrix.TransY / canvas.TotalMatrix.ScaleY);
canvas.DrawRect(10, start + fontMetrics.Ascent, 5, end - start + (fontMetrics.Bottom - fontMetrics.Ascent), paint);
canvas.Restore();
});
});
});
}
[Test]
public void InconsistentLineHeightWhenUsingNewLineTest()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Background(Colors.Grey.Lighten4)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(16));
text.Line(Placeholders.Paragraph());
text.Line("");
text.Line(Placeholders.Paragraph());
text.Line(Placeholders.Label()).FontSize(48);
text.Line(Placeholders.Paragraph());
text.Line("");
text.Line(Placeholders.Paragraph());
});
.Text("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا")
.FontSize(20);
});
}
}

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -7,11 +7,11 @@ using QuestPDF.ReportSample.Layouts;
//ImagePlaceholder.Solid = true;
// var model = DataSource.GetReport();
// var report = new StandardReport(model);
// report.ShowInPreviewer();
//
// return;
var model = DataSource.GetReport();
var report = new StandardReport(model);
report.ShowInPreviewer();
return;
Document
.Create(container =>
+344
View File
@@ -0,0 +1,344 @@
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using DynamicData;
using SkiaSharp;
namespace QuestPDF.Previewer;
class InteractiveCanvas : ICustomDrawOperation
{
public Rect Bounds { get; set; }
public ICollection<PreviewPage> Pages { get; set; }
public InspectionElement? InspectionElement { get; set; }
private float ViewportWidth => (float)Bounds.Width;
private float ViewportHeight => (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 - ViewportHeight / Scale;
public float ScrollPercentY
{
get
{
return TranslateY / MaxTranslateY;
}
set
{
TranslateY = value * MaxTranslateY;
}
}
public float ScrollViewportSizeY
{
get
{
var viewPortSize = ViewportHeight / 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 > ViewportHeight / Scale)
{
TranslateY = Math.Min(TranslateY, MaxTranslateY);
TranslateY = Math.Max(TranslateY, 0);
}
else
{
TranslateY = (TotalPagesHeight - ViewportHeight / Scale) / 2;
}
if (ViewportWidth / Scale < MaxWidth)
{
var maxTranslateX = (ViewportWidth / 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();
}
public int ActivePage { get; set; } = 1;
public IEnumerable<(int pageNumber, float beginY, float endY)> GetPagePosition()
{
var pageNumber = 1;
var currentPagePosition = SafeZone / Scale;
foreach (var page in Pages)
{
yield return (pageNumber, currentPagePosition, currentPagePosition + page.Height);
currentPagePosition += page.Height + PageSpacing;
pageNumber++;
}
}
public void SetActivePage(float x, float y)
{
y /= Scale;
y += TranslateY;
ActivePage = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY).pageNumber;
}
public void ScrollToInspectionElement(InspectionElement element)
{
var location = element.Location.MinBy(x => x.PageNumber);
var pagePosition = GetPagePosition().ElementAt(location.PageNumber - 1);
var page = Pages.ElementAt(location.PageNumber - 1);
var widthScale = ViewportWidth / location.Width;
var heightScale = ViewportHeight / location.Height;
var targetScale = Math.Min(widthScale, heightScale);
targetScale *= 0.7f; // slightly zoom out to show entire element with padding
Scale = targetScale;
TranslateY = pagePosition.beginY + location.Top + location.Height / 2 - ViewportHeight / Scale / 2;
TranslateX = page.Width / 2 - location.Left - location.Width / 2;
}
public (int pageNumber, float x, float y)? FindClickedPointOnThePage(float x, float y)
{
x -= ViewportWidth / 2;
x /= Scale;
x += TranslateX;
y /= Scale;
y += TranslateY;
var location = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY);
if (location == default)
return null;
var page = Pages.ElementAt(location.pageNumber - 1);
x += page.Width / 2;
if (x < 0 || page.Width < x)
return null;
y -= location.beginY;
return (location.pageNumber, x, y);
}
#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(ViewportWidth / 2, 0);
canvas.Scale(Scale);
canvas.Translate(TranslateX, -TranslateY);
var topMatrix = canvas.TotalMatrix;;
var positions = GetPagePosition().ToList();
foreach (var pageIndex in Enumerable.Range(0, Pages.Count))
{
canvas.SetMatrix(topMatrix);
var page = Pages.ElementAt(pageIndex);
var position = positions.ElementAt(pageIndex);
canvas.Translate(-page.Width / 2f, position.beginY);
DrawBlankPage(canvas, page.Width, page.Height);
canvas.DrawPicture(page.Picture);
DrawInspectionElement(canvas, pageIndex + 1);
}
canvas.SetMatrix(topMatrix);
DrawActivePage(canvas);
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("#555");
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, ViewportWidth, InnerGradientSize, fogPaint);
}
#endregion
#region Interactivity
private void DrawActivePage(SKCanvas canvas)
{
if (ActivePage == default)
return;
var page = Pages.ElementAt(ActivePage - 1);
var pagePosition = GetPagePosition().ElementAt(ActivePage - 1);
var thickness = 6f / Scale;
using var strokePaint = new SKPaint
{
StrokeWidth = thickness,
IsStroke = true,
Color = SKColor.Parse("#000")
};
canvas.DrawRect(- page.Width / 2 -thickness / 2, pagePosition.beginY -thickness / 2, page.Width + thickness, page.Height + thickness, strokePaint);
}
private void DrawInspectionElement(SKCanvas canvas, int pageNumber)
{
if (InspectionElement == null || InspectionElement.Location == null)
return;
var location = InspectionElement.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
return;
var size = 6 / Scale;
size = Math.Min(size, 3);
using var strokePaint1 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
Color = SKColor.Parse("#42A5F5")
};
using var strokePaint2 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
PathEffect = SKPathEffect.CreateDash(new[] { size * 3, size * 3 }, 0),
Color = SKColor.Parse("#1E88E5")
};
using var backgroundPaint = new SKPaint
{
Color = SKColor.Parse("#442196F3"),
};
canvas.DrawRect(location.Left, location.Top, location.Width, location.Height, backgroundPaint);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint1);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint2);
}
#endregion
}
+45
View File
@@ -0,0 +1,45 @@
<UserControl 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.MyHierarchy">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Name="Highlight" Background="transparent" DoubleTapped="InputElement_OnDoubleTapped" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave" PointerPressed="Clicked"></Panel>
<Panel Grid.Row="0" Grid.Column="0" Name="Indentation"></Panel>
<Panel Grid.Row="0" Grid.Column="1" Name="Panel" Width="16" Height="16" Margin="0,0,8,0" Background="transparent" PointerPressed="Toggle" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="#AFFF" Name="Icon" />
</Canvas>
</Viewbox>
</Panel>
<Panel Grid.Row="0" Grid.Column="2">
<TextBlock Name="ElementName" FontSize="12" Foreground="#AFFF" Margin="0,6" IsHitTestVisible="False"></TextBlock>
</Panel>
<ItemsRepeater Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Name="Repeater" HorizontalCacheLength="1000000" VerticalCacheLength="100000">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:InspectionElement">
<previewer:MyHierarchy Hierarchy="{Binding}" OnSelected="MyHierarchy_OnOnSelected"></previewer:MyHierarchy>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
</UserControl>
+115
View File
@@ -0,0 +1,115 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Color = System.Drawing.Color;
namespace QuestPDF.Previewer
{
public partial class MyHierarchy : UserControl
{
public static readonly StyledProperty<InspectionElement> HierarchyProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement?> SelectionProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement?>(nameof(Selection));
public InspectionElement? Selection
{
get => GetValue(SelectionProperty);
set => SetValue(SelectionProperty, value);
}
public event Action<InspectionElement>? OnSelected;
public bool Extended { get; set; }
public MyHierarchy()
{
InitializeComponent();
HierarchyProperty.Changed.Subscribe(x =>
{
Configure();
});
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Configure();
}
private void Configure()
{
var panel = this.FindControl<Panel>("Panel");
panel.IsVisible = Hierarchy?.Children?.Any() ?? false;
this.FindControl<Panel>("Indentation").Width = (Hierarchy?.Level ?? 0) * 24;
UpdateIcon();
this.FindControl<TextBlock>("ElementName").Text = Hierarchy?.Text;
this.FindControl<TextBlock>("ElementName").Foreground = new SolidColorBrush(Avalonia.Media.Color.Parse(Hierarchy?.FontColor ?? "#FFF"));
}
private const string ExtendedIcon = "M7,15L12,10L17,15H7Z";
private const string CollapsedIcon = "M7,10L12,15L17,10H7Z";
private void Toggle(object? sender, PointerPressedEventArgs e)
{
Extended = !Extended;
UpdateIcon();
this.FindControl<ItemsRepeater>("Repeater").Items = Extended ? Hierarchy?.Children : Array.Empty<InspectionElement>();
}
private void UpdateIcon()
{
var iconControl = this.FindControl<Avalonia.Controls.Shapes.Path>("Icon");
iconControl.Data = new PathGeometry
{
Figures = PathFigures.Parse(Extended ? ExtendedIcon : CollapsedIcon)
};
}
private void InputElement_OnDoubleTapped(object? sender, RoutedEventArgs e)
{
Toggle(null, null);
}
private void Highlight_OnPointerEnter(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Parse("hand");
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#1FFF"));
}
private void Indentation_OnPointerLeave(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Default;
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#0000"));
}
private void Clicked(object? sender, PointerPressedEventArgs e)
{
OnSelected?.Invoke(Hierarchy);
}
private void MyHierarchy_OnOnSelected(InspectionElement selected)
{
OnSelected?.Invoke(selected);
Selection = selected;
}
}
}
@@ -35,6 +35,22 @@ namespace QuestPDF.Previewer
set => SetValue(ScrollViewportSizeProperty, value);
}
public static readonly StyledProperty<InspectionElement> HierarchyProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement> CurrentSelectionProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(CurrentSelection));
public InspectionElement CurrentSelection
{
get => GetValue(CurrentSelectionProperty);
set => SetValue(CurrentSelectionProperty, value);
}
public PreviewerControl()
{
PagesProperty.Changed.Subscribe(x =>
@@ -49,7 +65,72 @@ namespace QuestPDF.Previewer
InvalidateVisual();
});
CurrentSelectionProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.InspectionElement = CurrentSelection;
//InteractiveCanvas.ScrollToInspectionElement(CurrentSelection);
InvalidateVisual();
});
ClipToBounds = true;
PointerPressed += (sender, args) =>
{
var position = args.GetPosition(this);
InteractiveCanvas.SetActivePage((float)position.X, (float)position.Y);
var clickedPosition = InteractiveCanvas.FindClickedPointOnThePage((float)position.X, (float)position.Y);
if (clickedPosition != null)
FindHighlightedElement(clickedPosition.Value.pageNumber, clickedPosition.Value.x, clickedPosition.Value.y);
InvalidateVisual();
};
}
void FindHighlightedElement(int pageNumber, float x, float y)
{
var possible = FlattenHierarchy(Hierarchy, 0)
.Select(x =>
{
var location = x.element.Location.First(y => y.PageNumber == pageNumber);
return new
{
Element = x.element,
Level = x.level,
Size = location.Width * location.Height
};
})
.ToList();
var minSize = possible.Min(x => x.Size);
CurrentSelection = possible
.Where(x => Math.Abs(x.Size - minSize) < 1)
.OrderByDescending(x => x.Level)
.First()
.Element;
IEnumerable<(InspectionElement element, int level)> FlattenHierarchy(InspectionElement element, int level)
{
var location = element.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
yield break;
if (x < location.Left || location.Left + location.Width < x)
yield break;
if (y < location.Top || location.Top + location.Height < y)
yield break;
yield return (element, level);
foreach (var childIndex in Enumerable.Range(0, element.Children.Count))
foreach (var nestedChild in FlattenHierarchy(element.Children[childIndex], level + childIndex + 1))
yield return nestedChild;
}
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+155
View File
@@ -0,0 +1,155 @@
<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"
xmlns:visualBasic="clr-namespace:Microsoft.VisualBasic;assembly=Microsoft.VisualBasic.Core"
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="56"
Background="#555"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector=".MainMenu TextBlock">
<Setter Property="Foreground" Value="#AFFF" />
<Setter Property="Margin" Value="0,0,16,0" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector=".MainMenu TextBlock:pointerover">
<Setter Property="Foreground" Value="#FFF" />
<Setter Property="TextDecorations" Value="Underline" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.actions">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Padding" Value="12" />
<Setter Property="Margin" Value="-1" />
<Setter Property="Background" Value="transparent"/>
</Style>
<Style Selector="Button.actions Path">
<Setter Property="Fill" Value="#8FFF"/>
</Style>
<Style Selector="Button.active Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#333"/>
</Style>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="56" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="300" MaxWidth="500" Width="300" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Background="#333">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/Logo.png" Width="32" Height="32" Margin="12,12,20,12"></Image>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock
VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,4"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="16" Foreground="#FFF" FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal" Classes="MainMenu">
<TextBlock Text="Getting Started" />
<TextBlock Text="Documentation" />
<TextBlock Text="GitHub" />
<TextBlock Text="Sponsor project" />
</StackPanel>
</StackPanel>
</StackPanel>
</Panel>
<Grid Grid.Row="1" Grid.Column="0" Background="#444">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Panel Grid.Row="0">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Document hierarchy" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="1">
<ScrollViewer>
<previewer:MyHierarchy Hierarchy="{Binding Items}" Margin="-8,0,0,0" Selection="{Binding SelectedItem, Mode=TwoWay}"></previewer:MyHierarchy>
</ScrollViewer>
</Panel>
<Panel Grid.Row="3">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Selected element properties" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="4">
<ItemsRepeater Items="{Binding SelectedItem.Metadata}" Margin="16, 2">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:Metadata">
<Grid Margin="0, 4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Label}" Foreground="#AFFF" FontSize="12" />
<TextBlock Grid.Column="2" Text="{Binding Value}" Foreground="#AFFF" FontSize="12" />
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Panel>
</Grid>
<GridSplitter Grid.Column="1" Grid.Row="1" Background="#444" ResizeDirection="Columns" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
CurrentSelection="{Binding SelectedItem, Mode=TwoWay}"
Hierarchy="{Binding Items}"
Pages="{Binding Pages, Mode=OneWay}" />
<ScrollBar Grid.Row="1" Grid.Column="3"
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>
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
@@ -0,0 +1,177 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.Json;
using Avalonia;
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/documentation/api-reference.html"));
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
LoadItems();
}
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();
}
private InspectionElement _selectedItem;
public InspectionElement SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
private InspectionElement items;
public InspectionElement Items
{
get => items;
set => this.RaiseAndSetIfChanged(ref items, value);
}
public void LoadItems()
{
var text = File.ReadAllText("hierarchy.json");
Items = JsonSerializer.Deserialize<InspectionElement>(text);
UpdateLevel(Items);
}
private void UpdateLevel(InspectionElement root)
{
var currentLevel = 1;
Traverse(root);
void Traverse(InspectionElement element)
{
element.Level = currentLevel;
currentLevel++;
element.Children.ForEach(Traverse);
currentLevel--;
}
}
}
public class InspectionElementLocation
{
public int PageNumber { get; set; }
public float Top { get; set; }
public float Left { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
public class Metadata
{
public string Label { get; set; }
public string Value { get; set; }
public Metadata(string label, string value)
{
Label = label;
Value = value;
}
}
public class InspectionElement
{
public string Element { get; set; }
public List<InspectionElementLocation> Location { get; set; }
public Dictionary<string, string> Properties { get; set; }
public List<InspectionElement> Children { get; set; }
public int Level { get; set; }
public string FontColor => Element == "DebugPointer" ? "#FFF" : "#AFFF";
public string Text => Element == "DebugPointer" ? Properties.First(x => x.Key == "Target").Value : Element;
public bool Expanded { get; set; }
public IList<Metadata> Metadata => ListMetadata().ToList();
public IEnumerable<Metadata> ListMetadata()
{
yield return new Metadata("Element name", Element);
yield return new Metadata("Position left", Location[0].Left.ToString("N2"));
yield return new Metadata("Position top", Location[0].Top.ToString("N2"));
yield return new Metadata("Width", Location[0].Width.ToString("N2"));
yield return new Metadata("Height", Location[0].Height.ToString("N2"));
foreach (var property in Properties)
yield return new Metadata(property.Key, property.Value);
}
}
}
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.12.0</Version>
<Version>2022.6.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>
@@ -13,7 +13,6 @@
<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>
@@ -34,22 +33,26 @@
<ItemGroup>
<EmbeddedResource Include="Resources\Logo.png" />
<None Include="Resources\Logo.png" Pack="true" PackagePath="\" />
<None Include="Resources\PackageReadme.md" Pack="true" PackagePath="\" />
<None Include="Resources\Logo.png">
<Pack>true</Pack>
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
<None Update="hierarchy.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</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="Avalonia" Version="0.10.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.80.4" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
</Project>

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long
@@ -31,7 +31,7 @@ namespace QuestPDF.ReportSample
public static string FormatAsRomanNumeral(this int number)
{
if (number < 0 || number > 3999)
throw new ArgumentOutOfRangeException(nameof(number), "Number should be in range from 1 to 3999");
throw new ArgumentOutOfRangeException("Number should be in range from 1 to 3999");
return RomanNumeralCache.GetOrAdd(number, x =>
{
@@ -68,11 +68,9 @@ namespace QuestPDF.ReportSample.Layouts
var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x =>
{
var formatted = x == 1 ? "1 page long" : $"{x} pages long";
return $" ({formatted})";
});
text.Span(" (").Style(lengthStyle);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
text.Span(")").Style(lengthStyle);
});
});
}
+75
View File
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Engines;
using BenchmarkDotNet.Running;
using Microsoft.VisualStudio.TestPlatform.TestHost;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.ReportSample.Layouts;
namespace QuestPDF.ReportSample
{
[SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 256)]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class PerformanceTests
{
private PageContext PageContext { get; set; }
private DocumentMetadata Metadata { get; set; }
private Container Content { get; set; }
[Test]
public void Run()
{
ImagePlaceholder.Solid = true;
var configuration = ManualConfig
.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
BenchmarkRunner.Run<PerformanceTests>(configuration);
}
[IterationSetup]
public void GenerateReportData()
{
ImagePlaceholder.Solid = true;
var model = DataSource.GetReport();
var report = new StandardReport(model);
Metadata = report.GetMetadata();
var documentContainer = new DocumentContainer();
report.Compose(documentContainer);
Content = documentContainer.Compose();
PageContext = new PageContext();
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
var sw = new Stopwatch();
sw.Start();
Content.VisitChildren(x =>
{
if (x is ICacheable)
x.CreateProxy(y => new CacheProxy(y));
});
sw.Stop();
Console.WriteLine($"Creating cache took: {sw.ElapsedMilliseconds}");
}
[Benchmark]
public void GenerationTest()
{
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
}
}
}
@@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<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" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
<ItemGroup>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Before

Width:  |  Height:  |  Size: 993 B

After

Width:  |  Height:  |  Size: 993 B

@@ -38,5 +38,21 @@ namespace QuestPDF.ReportSample
Report.GenerateXps(path);
Process.Start("explorer.exe", path);
}
[Test]
public void Profile()
{
ImagePlaceholder.Solid = true;
var container = new DocumentContainer();
Report.Compose(container);
var content = container.Compose();
var metadata = Report.GetMetadata();
var pageContext = new PageContext();
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
}
}
}
@@ -6,8 +6,8 @@ namespace QuestPDF.ReportSample
{
public static class Typography
{
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Verdana).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
}
}
@@ -86,43 +86,5 @@ 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();
}
}
}
@@ -153,9 +153,7 @@ namespace QuestPDF.UnitTests
Ratio = 2f
})
.DrawElement(new Size(500, 200))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -170,45 +168,7 @@ namespace QuestPDF.UnitTests
Ratio = 2f
})
.DrawElement(new Size(400, 300))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
[Test]
public void DrawChild_PerWidth_RightToLeft()
{
TestPlan
.For(x => new AspectRatio
{
Child = x.CreateChild(),
Option = AspectRatioOption.FitArea,
Ratio = 2f,
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(500, 200))
.ExpectCanvasTranslate(100, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(-100, 0)
.CheckDrawResult();
}
[Test]
public void DrawChild_PerHeight_RightToLeft()
{
TestPlan
.For(x => new AspectRatio
{
Child = x.CreateChild(),
Option = AspectRatioOption.FitArea,
Ratio = 2f,
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(400, 300))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
}
@@ -35,9 +35,7 @@ namespace QuestPDF.UnitTests
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(200, 100))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -51,43 +49,7 @@ namespace QuestPDF.UnitTests
})
.MeasureElement(new Size(500, 400))
.ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(300, 200))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(300, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
[Test]
public void Measure_PartialRender_RightToLeft()
{
TestPlan
.For(x => new MinimalBox
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
.ExpectCanvasTranslate(200, 0)
.ExpectChildDraw(new Size(200, 100))
.ExpectCanvasTranslate(-200, 0)
.CheckDrawResult();
}
[Test]
public void Measure_FullRender_RightToLeft()
{
TestPlan
.For(x => new MinimalBox
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.MeasureElement(new Size(500, 400))
.ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(350, 200))
.ExpectCanvasTranslate(150, 0)
.ExpectChildDraw(new Size(350, 200))
.ExpectCanvasTranslate(-150, 0)
.CheckDrawResult();
}
}
+62
View File
@@ -0,0 +1,62 @@
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using QuestPDF.UnitTests.TestEngine;
using SkiaSharp;
namespace QuestPDF.UnitTests
{
[TestFixture]
public class ImageTests
{
[Test]
public void Measure_TakesAvailableSpaceRegardlessOfSize()
{
TestPlan
.For(x => new Image
{
InternalImage = GenerateImage(400, 300)
})
.MeasureElement(new Size(300, 200))
.CheckMeasureResult(SpacePlan.FullRender(300, 200));
}
[Test]
public void Draw_TakesAvailableSpaceRegardlessOfSize()
{
TestPlan
.For(x => new Image
{
InternalImage = GenerateImage(400, 300)
})
.DrawElement(new Size(300, 200))
.ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
.CheckDrawResult();
}
[Test]
public void Fluent_RecognizesImageProportions()
{
var image = GenerateImage(600, 200).Encode(SKEncodedImageFormat.Png, 100).ToArray();
TestPlan
.For(x =>
{
var container = new Container();
container.Image(image);
return container;
})
.MeasureElement(new Size(300, 200))
.CheckMeasureResult(SpacePlan.FullRender(300, 100));;
}
SKImage GenerateImage(int width, int height)
{
var imageInfo = new SKImageInfo(width, height);
using var surface = SKSurface.Create(imageInfo);
return surface.Snapshot();
}
}
}

Some files were not shown because too many files have changed in this diff Show More