Compare commits

...

40 Commits

Author SHA1 Message Date
MarcinZiabek d39819bf8d Optimized drawing border 2021-12-26 21:32:49 +01:00
Marcin Ziąbek 5f636beb9d Added missing assertions to test that are supposed to throw an exception 2021-12-09 17:10:45 +01:00
Marcin Ziąbek 10683776bb 2021.12.0 2021-12-06 09:28:02 +01:00
Marcin Ziąbek f2c47c4fd3 2021.12.0-alpha1 2021-11-30 00:34:04 +01:00
Marcin Ziąbek 77b8f04659 Synced to 2021.12-alpha0 2021-11-19 00:50:38 +01:00
Marcin Ziąbek 5235ada66f Merge branch 'main' into element-proxy
# Conflicts:
#	QuestPDF/Drawing/PdfCanvas.cs
#	QuestPDF/QuestPDF.csproj
2021-11-19 00:46:46 +01:00
Marcin Ziąbek 4ce66878f2 2021.12.0-alpha0 2021-11-19 00:24:32 +01:00
Marcin Ziąbek 79cc5f1507 Added support for XPS rendering 2021-11-19 00:22:45 +01:00
Marcin Ziąbek 7c7d461c4b Merge pull request #53 from donmurta/main
Add simple chart example
2021-11-14 23:09:27 +01:00
Marcin Ziąbek 63af713935 2021.11.4 - Fixed measuring the Inlined element 2021-11-14 23:03:22 +01:00
Don Murta 2419a25cbd Add simple chart example 2021-11-09 10:26:46 -07:00
Marcin Ziąbek 014ec15908 2021.11.3 2021-11-07 16:46:34 +01:00
Marcin Ziąbek 82e29d5fdd ImagePlaceholder.Solid = true; 2021-11-07 16:19:54 +01:00
Marcin Ziąbek c64754241d Updated base to 2021.11 2021-11-04 01:02:44 +01:00
Marcin Ziąbek 9de87061e4 Merge branch 'main' into element-proxy
# Conflicts:
#	QuestPDF.ReportSample/Layouts/PhotoTemplate.cs
#	QuestPDF.ReportSample/Layouts/SectionTemplate.cs
#	QuestPDF.ReportSample/Tests.cs
#	QuestPDF/Drawing/DocumentGenerator.cs
#	QuestPDF/Elements/Constrained.cs
#	QuestPDF/Infrastructure/Position.cs
#	QuestPDF/Infrastructure/Size.cs
2021-11-04 00:22:22 +01:00
Marcin Ziąbek 86cfd22316 2021.11.0-beta3 2021-11-03 21:39:20 +01:00
Marcin Ziąbek 520bfc840c Improved code benchmarking (removed an unnecessary cost of image placeholder generation) 2021-11-03 21:22:08 +01:00
Marcin Ziąbek 22cdad69b7 Text element improvement: null string behaves like an empty text, not throws an exception 2021-11-03 21:21:23 +01:00
Marcin Ziąbek d260fa7587 Added generating symbols 2021-10-29 01:30:28 +02:00
Marcin Ziąbek 011668fb4c Image: added support for common overloads 2021-10-29 01:24:56 +02:00
Marcin Ziąbek 5a886452cf Merge pull request #40 from QuestPDF/2021.11
2021.11
2021-10-23 03:07:56 +02:00
Marcin Ziąbek b044208195 Updated nuget details 2021-10-23 03:02:18 +02:00
Marcin Ziąbek dadb8a5087 Improved test engine. Added more examples 2021-10-23 02:59:47 +02:00
Marcin Ziąbek 11f0809efc Improved default text style 2021-10-23 02:59:29 +02:00
Marcin Ziąbek 0a6673dd4f Update QuestPDF.csproj 2021-10-21 15:46:31 +02:00
Marcin Ziąbek 525bf534e5 Update DocumentGenerator.cs 2021-10-19 00:37:07 +02:00
Marcin Ziąbek fd696c5b66 Added missing tests to the constrained element 2021-10-12 23:58:28 +02:00
Marcin Ziąbek 0ccc8cc236 Improved the DocumentLayoutException message 2021-10-11 23:01:26 +02:00
Marcin Ziąbek 9ef444462a Implemented the Inlined document 2021-10-11 22:48:44 +02:00
Marcin Ziąbek 479915b68f Implemented the Inlined element 2021-10-11 01:48:12 +02:00
Marcin Ziąbek 5deea91fe4 Code refactorization 2021-10-10 23:21:11 +02:00
Marcin Ziąbek 7174a2afa1 Code style improvements 2021-10-10 19:17:31 +02:00
Marcin Ziąbek 1371fe1df9 Merge pull request #32 from QuestPDF/global-text-style
Global text style
2021-10-10 18:40:19 +02:00
Marcin Ziąbek bb00dfd4d4 Merge pull request #28 from jcl86/main
SkipOnce element for different headers and footers
2021-10-10 18:39:15 +02:00
Marcin Ziąbek 70e5783f0e Updated report sample to follow new text style approach 2021-10-07 23:23:07 +02:00
Marcin Ziąbek e8b7f38187 Fixed setting text style in the text-block-elements 2021-10-07 23:10:12 +02:00
Marcin Ziąbek c2ba2addfc Fixed handling global text style when setting block-wide style 2021-10-07 22:56:51 +02:00
Marcin Ziąbek e379502a64 Implemented prototype of global, document-wide text style 2021-10-07 22:38:41 +02:00
jcl-aadlab caa779e1e0 Added skip element for headers and footers 2021-10-05 10:50:55 +02:00
Marcin Ziąbek 4938e97e6d Update readme.md 2021-10-01 14:09:25 +02:00
82 changed files with 1966 additions and 409 deletions
-1
View File
@@ -18,7 +18,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(400, 100)
.FileName()
.ShowResults()
.Render(container =>
{
-1
View File
@@ -15,7 +15,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
+85
View File
@@ -0,0 +1,85 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using Microcharts;
using SkiaSharp;
namespace QuestPDF.Examples
{
public class ChartExample
{
[Test]
public void MicrochartChart()
{
var entries = new[]
{
new ChartEntry(212)
{
Label = "UWP",
ValueLabel = "112",
Color = SKColor.Parse("#2c3e50")
},
new ChartEntry(248)
{
Label = "Android",
ValueLabel = "648",
Color = SKColor.Parse("#77d065")
},
new ChartEntry(128)
{
Label = "iOS",
ValueLabel = "428",
Color = SKColor.Parse("#b455b6")
},
new ChartEntry(514)
{
Label = "Forms",
ValueLabel = "214",
Color = SKColor.Parse("#3498db")
}
};
RenderingTest
.Create()
.PageSize(400, 600)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Background(Colors.White)
.Padding(25)
.Stack(stack =>
{
stack
.Item()
.PaddingBottom(10)
.Text("Chart example", TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium));
stack
.Item()
.Border(1)
.ExtendHorizontal()
.Height(300)
.Canvas((canvas, size) =>
{
var chart = new BarChart
{
Entries = entries,
LabelOrientation = Orientation.Horizontal,
ValueLabelOrientation = Orientation.Horizontal,
IsAnimated = false,
};
chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
});
});
});
}
}
}
@@ -0,0 +1,56 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ComplexLayoutBenchmark
{
[Test]
public void ComplexLayout()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProducePdf()
.ShowResults()
.Render(x => GenerateStructure(x, 16));
}
private void GenerateStructure(IContainer container, int level)
{
if (level <= 0)
{
container.Background(Placeholders.BackgroundColor()).Height(10);
return;
}
level--;
if (level % 3 == 0)
{
container
.Border(level / 4f)
.BorderColor(Colors.Black)
.Row(row =>
{
row.RelativeColumn().Element(x => GenerateStructure(x, level));
row.RelativeColumn().Element(x => GenerateStructure(x, level));
});
}
else
{
container
.Border(level / 4f)
.BorderColor(Colors.Black)
.Stack(stack =>
{
stack.Item().Element(x => GenerateStructure(x, level));
stack.Item().Element(x => GenerateStructure(x, level));
});
}
}
}
}
+61 -18
View File
@@ -1,4 +1,5 @@
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
@@ -10,24 +11,66 @@ namespace QuestPDF.Examples
[Test]
public void Stack()
{
RenderingTest
.Create()
.PageSize(500, 360)
.FileName()
.Render(container =>
{
container
.Background(Colors.White)
.Padding(15)
.Grid(grid =>
{
grid.Spacing(15);
grid.Item().Background(Colors.Grey.Medium).Text(Placeholders.LoremIpsum());
grid.Item().DebugPointer("Test").Background(Colors.Grey.Lighten1).Height(1000); // <-- problem
grid.Item().Background(Colors.Grey.Lighten2).Height(150);
});
});
Assert.Throws<DocumentLayoutException>(() =>
{
RenderingTest
.Create()
.PageSize(500, 360)
.Render(container =>
{
container
.Padding(10)
.Width(100)
.Background(Colors.Grey.Lighten3)
.DebugPointer("Example debug pointer")
.Stack(x =>
{
x.Item().Text("Test");
x.Item().Width(150);
});
});
});
}
[Test]
public void Simple()
{
Assert.Throws<DocumentLayoutException>(() =>
{
RenderingTest
.Create()
.PageSize(500, 360)
.Render(container =>
{
container
.Padding(10)
.Width(100)
.Background(Colors.Grey.Lighten3)
.Width(150)
.Text("Test");
});
});
}
[Test]
public void DebugPointer()
{
Assert.Throws<DocumentLayoutException>(() =>
{
RenderingTest
.Create()
.PageSize(500, 360)
.Render(container =>
{
container
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Width(100)
.DebugPointer("Example debug pointer")
.Width(150)
.Text("Example");
});
});
}
}
}
@@ -0,0 +1,48 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class DefaultTextStyleExample
{
[Test]
public void DefaultTextStyle()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
// all text in this set of pages has size 20
page.DefaultTextStyle(TextStyle.Default.Size(20));
page.Margin(20);
page.Size(PageSizes.A4);
page.Background(Colors.White);
page.Content().Stack(stack =>
{
stack.Item().Text(Placeholders.Sentence());
stack.Item().Text(text =>
{
// text in this block is additionally semibold
text.DefaultTextStyle(TextStyle.Default.SemiBold());
text.Line(Placeholders.Sentence());
// this text has size 20 but also semibold and red
text.Span(Placeholders.Sentence(), TextStyle.Default.Color(Colors.Red.Medium));
});
});
});
});
}
}
}
-25
View File
@@ -17,7 +17,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(200, 150)
.FileName()
.Render(container =>
{
container
@@ -33,7 +32,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
@@ -63,7 +61,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(740, 200)
.FileName()
.Render(container =>
{
container
@@ -104,7 +101,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(740, 200)
.FileName()
.Render(container =>
{
container
@@ -126,7 +122,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 360)
.FileName()
.Render(container =>
{
container
@@ -149,7 +144,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(210, 210)
.FileName()
.Render(container =>
{
container
@@ -172,7 +166,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 200)
.FileName()
.Render(container =>
{
var text = "";
@@ -195,7 +188,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(400, 230)
.FileName()
.Render(container =>
{
var textStyle = TextStyle.Default.Size(14);
@@ -230,7 +222,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 200)
.FileName()
.Render(container =>
{
container
@@ -260,7 +251,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(400, 250)
.FileName()
.Render(container =>
{
container
@@ -336,7 +326,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
@@ -381,7 +370,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(450, 150)
.FileName()
.Render(container =>
{
container
@@ -414,7 +402,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 175)
.FileName()
.Render(container =>
{
container
@@ -441,7 +428,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
@@ -481,7 +467,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 150)
.FileName()
.Render(container =>
{
container
@@ -502,7 +487,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 175)
.FileName()
.Render(container =>
{
container
@@ -554,7 +538,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 200)
.FileName()
.Render(container =>
{
container
@@ -582,7 +565,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(650, 450)
.FileName()
.Render(container =>
{
container
@@ -621,7 +603,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
@@ -648,7 +629,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
@@ -680,7 +660,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(350, 350)
.FileName()
.Render(container =>
{
container
@@ -722,7 +701,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(200, 200)
.FileName()
.Render(container =>
{
container
@@ -748,7 +726,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(400, 350)
.FileName()
.Render(container =>
{
container
@@ -785,7 +762,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 225)
.FileName()
.Render(container =>
{
container
@@ -839,7 +815,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(600, 310)
.FileName()
.Render(container =>
{
container
+28 -7
View File
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using QuestPDF.Elements;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples.Engine
@@ -17,6 +18,7 @@ namespace QuestPDF.Examples.Engine
{
private string FileNamePrefix = "test";
private Size Size { get; set; }
private int? MaxPagesThreshold { get; set; }
private bool ShowResult { get; set; }
private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
@@ -25,12 +27,12 @@ namespace QuestPDF.Examples.Engine
}
public static RenderingTest Create()
public static RenderingTest Create([CallerMemberName] string fileName = "test")
{
return new RenderingTest();
return new RenderingTest().FileName(fileName);
}
public RenderingTest FileName([CallerMemberName] string fileName = "test")
public RenderingTest FileName(string fileName)
{
FileNamePrefix = fileName;
return this;
@@ -67,12 +69,31 @@ namespace QuestPDF.Examples.Engine
public void Render(Action<IContainer> content)
{
var container = new Container();
content(container);
RenderDocument(container =>
{
container.Page(page =>
{
page.Size(new PageSize(Size.Width, Size.Height));
page.Content().Container().Background(Colors.White).Element(content);
});
});
}
public void MaxPages(int value)
{
MaxPagesThreshold = value;
}
var maxPages = ResultType == RenderingTestResult.Pdf ? 1000 : 10;
var document = new SimpleDocument(container, Size, maxPages);
public void RenderDocument(Action<IDocumentContainer> content)
{
MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10;
var document = new SimpleDocument(content, MaxPagesThreshold.Value);
Render(document);
}
private void Render(IDocument document)
{
if (ResultType == RenderingTestResult.Images)
{
Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
+6 -11
View File
@@ -1,4 +1,5 @@
using QuestPDF.Drawing;
using System;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
@@ -10,14 +11,12 @@ namespace QuestPDF.Examples.Engine
{
public const int ImageScalingFactor = 2;
private IContainer Container { get; }
private Size Size { get; }
private Action<IDocumentContainer> Content { get; }
private int MaxPages { get; }
public SimpleDocument(IContainer container, Size size, int maxPages)
public SimpleDocument(Action<IDocumentContainer> content, int maxPages)
{
Container = container;
Size = size;
Content = content;
MaxPages = maxPages;
}
@@ -32,11 +31,7 @@ namespace QuestPDF.Examples.Engine
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(new PageSize(Size.Width, Size.Height));
page.Content().Container().Background(Colors.White).Element(Container as Container);
});
Content(container);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class EnsureSpaceExample
{
[Test]
public void EnsureSpaceWith()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.Header().Text("With ensure space", TextStyle.Default.SemiBold());
page.Content().Stack(stack =>
{
stack
.Item()
.ExtendHorizontal()
.Height(75)
.Background(Colors.Grey.Lighten2);
stack
.Item()
.EnsureSpace(100)
.Text(Placeholders.LoremIpsum());
});
page.Footer().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" out of ");
text.TotalPages();
});
});
});
}
}
}
-1
View File
@@ -28,7 +28,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(550, 400)
.FileName()
.ShowResults()
.Render(container =>
{
+51
View File
@@ -0,0 +1,51 @@
using System.IO;
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class ImageExamples
{
[Test]
public void LoadingImage()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ProducePdf()
.ShowResults()
.Render(page =>
{
page.Padding(25).Stack(stack =>
{
stack.Spacing(25);
stack.Item().Image("logo.png");
var binaryData = File.ReadAllBytes("logo.png");
stack.Item().Image(binaryData);
using var stream = new FileStream("logo.png", FileMode.Open);
stack.Item().Image(stream);
});
});
}
[Test]
public void Exception()
{
Assert.Throws<DocumentComposeException>(() =>
{
RenderingTest
.Create()
.PageSize(PageSizes.A2)
.ProducePdf()
.ShowResults()
.Render(page => page.Image("non_existent.png"));
});
}
}
}
+123
View File
@@ -0,0 +1,123 @@
using System;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class InlinedExamples
{
[Test]
public void Inlined()
{
RenderingTest
.Create()
.PageSize(800, 650)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.Decoration(decoration =>
{
decoration.Header().Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.Size(20));
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
decoration
.Content()
.PaddingTop(25)
//.Box()
.Border(1)
.Background(Colors.Grey.Lighten2)
.Inlined(inlined =>
{
inlined.Spacing(25);
inlined.AlignSpaceAround();
inlined.BaselineMiddle();
var random = new Random(123);
foreach (var _ in Enumerable.Range(0, 50))
{
var width = random.Next(2, 7);
var height = random.Next(2, 7);
var sizeText = $"{width}×{height}";
inlined
.Item()
.Border(1)
.Width(width * 25)
.Height(height * 25)
.Background(Placeholders.BackgroundColor())
.Layers(layers =>
{
layers.Layer().Grid(grid =>
{
grid.Columns(width);
Enumerable.Range(0, width * height).ToList().ForEach(x => grid.Item().Border(1).BorderColor(Colors.White).Width(25).Height(25));
});
layers
.PrimaryLayer()
.AlignCenter()
.AlignMiddle()
.Text(sizeText, TextStyle.Default.Size(15));
});
}
});
});
});
}
[Test]
public void Inline_AlignLeft_BaselineBottom()
{
RenderingTest
.Create()
.PageSize(800, 600)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Box()
.Border(1)
.Background(Colors.Grey.Lighten4)
.Inlined(inlined =>
{
inlined.VerticalSpacing(50);
inlined.HorizontalSpacing(25);
inlined.AlignRight();
inlined.BaselineMiddle();
foreach (var _ in Enumerable.Range(0, 100))
inlined.Item().Element(RandomBlock);
});
});
void RandomBlock(IContainer container)
{
container
.Width(Placeholders.Random.Next(1, 5) * 20)
.Height(Placeholders.Random.Next(1, 5) * 20)
.Border(1)
.BorderColor(Colors.Grey.Darken2)
.Background(Placeholders.BackgroundColor());
}
}
}
}
+2 -1
View File
@@ -36,7 +36,8 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(350, 280)
.FileName()
.ProducePdf()
.ShowResults()
.Render(container =>
{
container
-4
View File
@@ -13,7 +13,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.ShowResults()
.Render(container =>
{
@@ -37,7 +36,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(200, 150)
.FileName()
.Render(container =>
{
container
@@ -62,7 +60,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(200, 150)
.FileName()
.Render(container =>
{
container
@@ -113,7 +110,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(200, 150)
.FileName()
.Render(container =>
{
container
@@ -6,6 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
@@ -23,6 +24,9 @@
<None Update="LibreBarcode39-Regular.ttf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="logo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+54
View File
@@ -0,0 +1,54 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ShowOnceExample
{
[Test]
public void ShowOnce()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.Header().Text("With show once", TextStyle.Default.SemiBold());
page.Content().PaddingVertical(5).Row(row =>
{
row.RelativeColumn()
.Background(Colors.Grey.Lighten2)
.Border(1)
.Padding(5)
.ShowOnce()
.Text(Placeholders.Label());
row.RelativeColumn(2)
.Border(1)
.Padding(5)
.Text(Placeholders.Paragraph());
});
page.Footer().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" out of ");
text.TotalPages();
});
});
});
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class SkipOnceExample
{
[Test]
public void SkipOnce()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(20);
page.Size(PageSizes.A7.Landscape());
page.Background(Colors.White);
page.Header().Stack(stack =>
{
stack.Item().ShowOnce().Text("This header is visible on the first page.");
stack.Item().SkipOnce().Text("This header is visible on the second page and all following.");
});
page.Content()
.PaddingVertical(10)
.Text(Placeholders.Paragraphs(), TextStyle.Default.Color(Colors.Grey.Medium));
page.Footer().Text(text =>
{
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" out of ");
text.TotalPages();
});
});
});
}
}
}
+1 -3
View File
@@ -21,7 +21,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ShowResults()
.Render(x => ComposeBook(x, chapters));
@@ -43,8 +42,7 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ProducePdf()
.Render(x => ComposeBook(x, chapters));
}
+1 -7
View File
@@ -17,7 +17,7 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 300)
.FileName()
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -42,7 +42,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 300)
.FileName()
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -71,7 +70,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(500, 200)
.FileName()
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -103,7 +101,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ShowResults()
.Render(container =>
@@ -134,7 +131,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ShowResults()
.Render(container =>
@@ -164,7 +160,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ShowResults()
.Render(container =>
@@ -228,7 +223,6 @@ namespace QuestPDF.Examples
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.ShowResults()
.Render(container =>
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+2 -14
View File
@@ -7,9 +7,6 @@ namespace QuestPDF.ReportSample
{
public static class DataSource
{
public static int SectionCounter { get; set; }
public static int FieldCounter { get; set; }
public static ReportModel GetReport()
{
return new ReportModel
@@ -89,7 +86,6 @@ namespace QuestPDF.ReportSample
return new ReportSectionMap
{
Label = "Location",
ImageSource = Placeholders.Image,
Location = Helpers.RandomLocation()
};
}
@@ -99,10 +95,7 @@ namespace QuestPDF.ReportSample
return new ReportSectionPhotos
{
Label = "Photos",
Photos = Enumerable
.Range(0, Helpers.Random.Next(1, 15))
.Select(x => Placeholders.Image(400, 300))
.ToList()
PhotoCount = Helpers.Random.Next(1, 15)
};
}
@@ -110,14 +103,9 @@ namespace QuestPDF.ReportSample
{
return new ReportPhoto()
{
PhotoData = Placeholders.Image(800, 600),
Comments = Placeholders.Sentence(),
Date = DateTime.Now - TimeSpan.FromDays(Helpers.Random.NextDouble() * 100),
Location = Helpers.RandomLocation(),
MapContextSource = x => Placeholders.Image(400, 300),
MapDetailsSource = x => Placeholders.Image(400, 300)
Location = Helpers.RandomLocation()
};
}
}
@@ -0,0 +1,85 @@
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.ReportSample.Layouts
{
public class DifferentHeadersTemplate : IDocument
{
public DocumentMetadata GetMetadata() => DocumentMetadata.Default;
public void Compose(IDocumentContainer container)
{
container
.Page(page =>
{
page.Margin(40);
page.Size(PageSizes.A4);
page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().Element(ComposeFooter);
});
}
private void ComposeHeader(IContainer container)
{
container.Background(Colors.Grey.Lighten3).Border(1).Stack(stack =>
{
stack.Item().ShowOnce().Padding(5).AlignMiddle().Row(row =>
{
row.RelativeColumn(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().Box().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30);
});
stack.Item().SkipOnce().Padding(5).Row(row =>
{
row.RelativeColumn(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().Box().Background(Colors.Blue.Lighten4).Padding(15);
});
});
}
private void ComposeContent(IContainer container)
{
container.Stack(stack =>
{
stack.Item().PaddingVertical(80).Text("First");
stack.Item().PageBreak();
stack.Item().PaddingVertical(80).Text("Second");
stack.Item().PageBreak();
stack.Item().PaddingVertical(80).Text("Third");
stack.Item().PageBreak();
});
}
private void ComposeFooter(IContainer container)
{
container.Background(Colors.Grey.Lighten3).Stack(stack =>
{
stack.Item().ShowOnce().Background(Colors.Grey.Lighten3).Row(row =>
{
row.RelativeColumn().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
row.RelativeColumn().AlignRight().Text("Footer for header");
});
stack.Item().SkipOnce().Background(Colors.Grey.Lighten3).Row(row =>
{
row.RelativeColumn().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
row.RelativeColumn().AlignRight().Text("Footer for every page except header");
});
});
}
}
}
@@ -0,0 +1,20 @@
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.ReportSample.Layouts
{
public class ImagePlaceholder : IComponent
{
public static bool Solid { get; set; } = false;
public void Compose(IContainer container)
{
if (Solid)
container.Background(Placeholders.Color());
else
container.Image(Placeholders.Image);
}
}
}
@@ -1,31 +0,0 @@
using System;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.ReportSample.Layouts
{
public class ImageTemplate : IComponent
{
private Func<Size, byte[]> Source { get; }
private float AspectRatio { get; }
public ImageTemplate(byte[] source, float aspectRatio = 1.333333f) : this(_ => source, aspectRatio)
{
}
public ImageTemplate(Func<Size, byte[]> source, float aspectRatio = 1.333333f)
{
Source = source;
AspectRatio = aspectRatio;
}
public void Compose(IContainer container)
{
container
.AspectRatio(AspectRatio)
.Background("#EEEEEE")
.Image(Source(Size.Zero));
}
}
}
@@ -30,14 +30,14 @@ namespace QuestPDF.ReportSample.Layouts
container
.Row(row =>
{
row.RelativeColumn(2).AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3);
row.RelativeColumn(2).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
row.RelativeColumn().PaddingLeft(5).Stack(stack =>
{
stack.Spacing(7f);
stack.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3);
stack.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3);
stack.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
stack.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>();
});
});
}
@@ -48,13 +48,13 @@ namespace QuestPDF.ReportSample.Layouts
{
grid.Columns(6);
grid.Item().LabelCell().Text("Date", Typography.Normal);
grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty, Typography.Normal);
grid.Item().LabelCell().Text("Location", Typography.Normal);
grid.Item(2).ValueCell().Text(Model.Location.Format(), Typography.Normal);
grid.Item().LabelCell().Text("Date");
grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty);
grid.Item().LabelCell().Text("Location");
grid.Item(2).ValueCell().Text(Model.Location.Format());
grid.Item().LabelCell().Text("Comments", Typography.Normal);
grid.Item(5).ValueCell().Text(Model.Comments, Typography.Normal);
grid.Item().LabelCell().Text("Comments");
grid.Item(5).ValueCell().Text(Model.Comments);
});
}
}
@@ -1,3 +1,4 @@
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
@@ -30,11 +31,11 @@ namespace QuestPDF.ReportSample.Layouts
{
stack.Item().EnsureSpace(25).Row(row =>
{
row.ConstantColumn(150).LabelCell().Text(part.Label, Typography.Normal);
row.ConstantColumn(150).LabelCell().Text(part.Label);
var frame = row.RelativeColumn().ValueCell();
if (part is ReportSectionText text)
frame.ShowEntire().Text(text.Text, Typography.Normal);
frame.ShowEntire().Text(text.Text);
if (part is ReportSectionMap map)
frame.Element(x => MapElement(x, map));
@@ -49,9 +50,9 @@ namespace QuestPDF.ReportSample.Layouts
void MapElement(IContainer container, ReportSectionMap model)
{
if (model.ImageSource == null || model.Location == null)
if (model.Location == null)
{
container.Text("No location provided", Typography.Normal);
container.Text("No location provided");
return;
}
@@ -59,14 +60,14 @@ namespace QuestPDF.ReportSample.Layouts
{
stack.Spacing(5);
stack.Item().MaxWidth(250).AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3);
stack.Item().Text(model.Location.Format(), Typography.Normal);
stack.Item().MaxWidth(250).AspectRatio(4 / 3f).Component<ImagePlaceholder>();
stack.Item().Text(model.Location.Format());
});
}
void PhotosElement(IContainer container, ReportSectionPhotos model)
{
if (model.Photos.Count == 0)
if (model.PhotoCount == 0)
{
container.Text("No photos", Typography.Normal);
return;
@@ -77,7 +78,10 @@ namespace QuestPDF.ReportSample.Layouts
grid.Spacing(5);
grid.Columns(3);
model.Photos.ForEach(x => grid.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3));
Enumerable
.Range(0, model.PhotoCount)
.ToList()
.ForEach(x => grid.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>());
});
}
}
@@ -27,6 +27,8 @@ namespace QuestPDF.ReportSample.Layouts
container
.Page(page =>
{
page.DefaultTextStyle(Typography.Normal);
page.MarginVertical(40);
page.MarginHorizontal(50);
@@ -37,8 +39,6 @@ namespace QuestPDF.ReportSample.Layouts
page.Footer().AlignCenter().Text(text =>
{
text.DefaultTextStyle(Typography.Normal);
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
@@ -55,7 +55,7 @@ namespace QuestPDF.ReportSample.Layouts
row.Spacing(50);
row.RelativeColumn().PaddingTop(-10).Text(Model.Title, Typography.Title);
row.ConstantColumn(150).ExternalLink("https://www.questpdf.com").Background(Colors.Grey.Lighten3);
row.ConstantColumn(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
});
stack.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
@@ -69,8 +69,8 @@ namespace QuestPDF.ReportSample.Layouts
{
grid.Item().Text(text =>
{
text.Span($"{field.Label}: ", Typography.Normal.SemiBold());
text.Span(field.Value, Typography.Normal);
text.Span($"{field.Label}: ", TextStyle.Default.SemiBold());
text.Span(field.Value);
});
}
});
@@ -41,9 +41,9 @@ namespace QuestPDF.ReportSample.Layouts
.InternalLink(locationName)
.Row(row =>
{
row.ConstantColumn(25).Text($"{number}.", Typography.Normal);
row.RelativeColumn().Text(locationName, Typography.Normal);
row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName, Typography.Normal));
row.ConstantColumn(25).Text($"{number}.");
row.RelativeColumn().Text(locationName);
row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName));
});
}
}
+2 -8
View File
@@ -44,22 +44,16 @@ namespace QuestPDF.ReportSample
public class ReportSectionMap : ReportSectionElement
{
public Func<Size, byte[]> ImageSource { get; set; }
public Location Location { get; set; }
}
public class ReportSectionPhotos : ReportSectionElement
{
public List<byte[]> Photos { get; set; }
public int PhotoCount { get; set; }
}
public class ReportPhoto
{
public byte[] PhotoData { get; set; }
public Func<Size, byte[]> MapDetailsSource { get; set; }
public Func<Size, byte[]> MapContextSource { get; set; }
{
public Location Location { get; set; }
public DateTime? Date { get; set; }
+5 -1
View File
@@ -16,7 +16,7 @@ using QuestPDF.ReportSample.Layouts;
namespace QuestPDF.ReportSample
{
[SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 100)]
[SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 256)]
[MinColumn, MaxColumn, MeanColumn, MedianColumn]
public class PerformanceTests
{
@@ -27,6 +27,8 @@ namespace QuestPDF.ReportSample
[Test]
public void Run()
{
ImagePlaceholder.Solid = true;
var configuration = ManualConfig
.Create(DefaultConfig.Instance)
.WithOptions(ConfigOptions.DisableOptimizationsValidator);
@@ -37,6 +39,8 @@ namespace QuestPDF.ReportSample
[IterationSetup]
public void GenerateReportData()
{
ImagePlaceholder.Solid = true;
var model = DataSource.GetReport();
var report = new StandardReport(model);
Metadata = report.GetMetadata();
+11 -2
View File
@@ -22,17 +22,26 @@ namespace QuestPDF.ReportSample
}
[Test]
public void GenerateAndShow()
public void GenerateAndShowPdf()
{
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
Report.GeneratePdf(path);
Process.Start("explorer.exe", path);
}
[Test]
public void GenerateAndShowXps()
{
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.xps");
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();
+1 -1
View File
@@ -8,6 +8,6 @@ namespace QuestPDF.ReportSample
{
public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Black).Size(11).LineHeight(1.1f);
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f);
}
}
+2 -2
View File
@@ -21,7 +21,7 @@ namespace QuestPDF.UnitTests
Color = Colors.Red.Medium
})
.DrawElement(new Size(400, 300))
.ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
.ExpectCanvasDrawFilledRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
.CheckDrawResult();
}
@@ -35,7 +35,7 @@ namespace QuestPDF.UnitTests
Child = x.CreateChild()
})
.DrawElement(new Size(400, 300))
.ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
.ExpectCanvasDrawFilledRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
.ExpectChildDraw(new Size(400, 300))
.CheckDrawResult();
}
+30 -6
View File
@@ -1,9 +1,11 @@
using NUnit.Framework;
using System.Drawing;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.UnitTests.TestEngine;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.UnitTests
{
@@ -32,7 +34,29 @@ namespace QuestPDF.UnitTests
}
[Test]
public void Draw_HorizontalRight_VerticalTop()
public void Draw_SameWidths_Optimized()
{
TestPlan
.For(x => new Border
{
Top = 10,
Right = 10,
Bottom = 10,
Left = 10,
Color = Colors.Red.Medium,
Child = x.CreateChild()
})
.DrawElement(new Size(400, 300))
.ExpectChildDraw(new Size(400, 300))
.ExpectCanvasDrawStrokedRectangle(new Size(400, 300), Colors.Red.Medium, 10)
.ExpectChildDraw(new Size(400, 300))
.CheckDrawResult();
}
[Test]
public void Draw_VariousWidths()
{
TestPlan
.For(x => new Border
@@ -48,10 +72,10 @@ namespace QuestPDF.UnitTests
})
.DrawElement(new Size(400, 300))
.ExpectChildDraw(new Size(400, 300))
.ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(430, 10), Colors.Red.Medium) // top
.ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(40, 320), Colors.Red.Medium) // left
.ExpectCanvasDrawRectangle(new Position(-20, 285), new Size(430, 30), Colors.Red.Medium) // bottom
.ExpectCanvasDrawRectangle(new Position(390, -5), new Size(20, 320), Colors.Red.Medium) // right
.ExpectCanvasDrawFilledRectangle(new Position(-20, -5), new Size(430, 10), Colors.Red.Medium) // top
.ExpectCanvasDrawFilledRectangle(new Position(-20, -5), new Size(40, 320), Colors.Red.Medium) // left
.ExpectCanvasDrawFilledRectangle(new Position(-20, 285), new Size(430, 30), Colors.Red.Medium) // bottom
.ExpectCanvasDrawFilledRectangle(new Position(390, -5), new Size(20, 320), Colors.Red.Medium) // right
.ExpectChildDraw(new Size(400, 300))
.CheckDrawResult();
}
+88
View File
@@ -9,6 +9,8 @@ namespace QuestPDF.UnitTests
[TestFixture]
public class ConstrainedTests
{
#region Height
[Test]
public void Measure_MinHeight_ExpectWrap()
{
@@ -88,5 +90,91 @@ namespace QuestPDF.UnitTests
.ExpectChildMeasure(new Size(400, 100), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
}
#endregion
#region Width
[Test]
public void Measure_MinWidth_ExpectWrap()
{
TestPlan
.For(x => new Constrained
{
MinWidth = 100
})
.MeasureElement(new Size(50, 400))
.CheckMeasureResult(SpacePlan.Wrap());
}
[Test]
public void Measure_MinWidth_ExtendHeight()
{
TestPlan
.For(x => new Constrained
{
MinWidth = 100,
Child = x.CreateChild()
})
.MeasureElement(new Size(200, 400))
.ExpectChildMeasure(new Size(200, 400), SpacePlan.FullRender(50, 400))
.CheckMeasureResult(SpacePlan.FullRender(100, 400));
}
[Test]
public void Measure_MinWidth_PassHeight()
{
TestPlan
.For(x => new Constrained
{
MinWidth = 100,
Child = x.CreateChild()
})
.MeasureElement(new Size(200, 400))
.ExpectChildMeasure(new Size(200, 400), SpacePlan.FullRender(150, 400))
.CheckMeasureResult(SpacePlan.FullRender(150, 400));
}
[Test]
public void Measure_MaxWidth_Empty()
{
TestPlan
.For(x => new Constrained
{
MaxWidth = 100
})
.MeasureElement(new Size(150, 400))
.CheckMeasureResult(SpacePlan.FullRender(0, 0));
}
[Test]
public void Measure_MaxWidth_PartialRender()
{
TestPlan
.For(x => new Constrained
{
MaxWidth = 100,
Child = x.CreateChild()
})
.MeasureElement(new Size(200, 400))
.ExpectChildMeasure(new Size(100, 400), SpacePlan.PartialRender(75, 400))
.CheckMeasureResult(SpacePlan.PartialRender(75, 400));
}
[Test]
public void Measure_MaxWidth_ExpectWrap()
{
TestPlan
.For(x => new Constrained
{
MaxWidth = 100,
Child = x.CreateChild()
})
.MeasureElement(new Size(200, 400))
.ExpectChildMeasure(new Size(100, 400), SpacePlan.Wrap())
.CheckMeasureResult(SpacePlan.Wrap());
}
#endregion
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ namespace QuestPDF.UnitTests
.ExpectCanvasDrawImage(Position.Zero, new Size(400, 300))
.CheckDrawResult();
passedSize.Should().Be(new Size(400, 300));
passedSize.Should().BeEquivalentTo(new Size(400, 300));
}
byte[] GenerateImage(Size size)
+4 -2
View File
@@ -11,13 +11,15 @@ namespace QuestPDF.UnitTests.TestEngine
public Action<float, float> ScaleFunc { get; set; }
public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
public Action<Position, Size, string> DrawRectFunc { get; set; }
public Action<Position, Size, string> DrawFilledRectangleFunc { get; set; }
public Action<Size, string, float> DrawStrokedRectangleFunc { get; set; }
public void Translate(Position vector) => TranslateFunc(vector);
public void Rotate(float angle) => RotateFunc(angle);
public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
public void DrawFilledRectangle(Position vector, Size size, string color) => DrawFilledRectangleFunc(vector, size, color);
public void DrawStrokedRectangle(Size size, string color, float width) => DrawStrokedRectangleFunc(size, color, width);
public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
@@ -14,7 +14,8 @@ namespace QuestPDF.UnitTests.TestEngine
public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
public void DrawFilledRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawFilledRectangleOperation(vector, size, color));
public void DrawStrokedRectangle(Size size, string color, float width) => Operations.Add(new CanvasDrawStrokedRectangleOperation(size, color, width));
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
@@ -2,13 +2,13 @@
namespace QuestPDF.UnitTests.TestEngine.Operations
{
internal class CanvasDrawRectangleOperation : OperationBase
internal class CanvasDrawFilledRectangleOperation : OperationBase
{
public Position Position { get; }
public Size Size { get; }
public string Color { get; }
public CanvasDrawRectangleOperation(Position position, Size size, string color)
public CanvasDrawFilledRectangleOperation(Position position, Size size, string color)
{
Position = position;
Size = size;
@@ -0,0 +1,18 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.UnitTests.TestEngine.Operations
{
internal class CanvasDrawStrokedRectangleOperation : OperationBase
{
public Size Size { get; }
public string Color { get; }
public float Width { get; set; }
public CanvasDrawStrokedRectangleOperation(Size size, string color, float width)
{
Size = size;
Color = color;
Width = width;
}
}
}
+24 -9
View File
@@ -70,17 +70,27 @@ namespace QuestPDF.UnitTests.TestEngine
Assert.AreEqual(expected.ScaleX, scaleX, "Scale X");
Assert.AreEqual(expected.ScaleY, scaleY, "Scale Y");
},
DrawRectFunc = (position, size, color) =>
DrawFilledRectangleFunc = (position, size, color) =>
{
var expected = GetExpected<CanvasDrawRectangleOperation>();
var expected = GetExpected<CanvasDrawFilledRectangleOperation>();
Assert.AreEqual(expected.Position.X, position.X, "Draw rectangle: X");
Assert.AreEqual(expected.Position.Y, position.Y, "Draw rectangle: Y");
Assert.AreEqual(expected.Position.X, position.X, "Draw filled rectangle: X");
Assert.AreEqual(expected.Position.Y, position.Y, "Draw filled rectangle: Y");
Assert.AreEqual(expected.Size.Width, size.Width, "Draw rectangle: width");
Assert.AreEqual(expected.Size.Height, size.Height, "Draw rectangle: height");
Assert.AreEqual(expected.Size.Width, size.Width, "Draw filled rectangle: width");
Assert.AreEqual(expected.Size.Height, size.Height, "Draw filled rectangle: height");
Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
Assert.AreEqual(expected.Color, color, "Draw filled rectangle: color");
},
DrawStrokedRectangleFunc = (size, color, width) =>
{
var expected = GetExpected<CanvasDrawStrokedRectangleOperation>();
Assert.AreEqual(expected.Size.Width, size.Width, "Draw stroked rectangle: width");
Assert.AreEqual(expected.Size.Height, size.Height, "Draw stroked rectangle: height");
Assert.AreEqual(expected.Color, color, "Draw stroked rectangle: color");
Assert.AreEqual(expected.Width, width, "Draw stroked rectangle: width");
},
DrawTextFunc = (text, position, style) =>
{
@@ -196,9 +206,14 @@ namespace QuestPDF.UnitTests.TestEngine
return AddOperation(new CanvasRotateOperation(angle));
}
public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, string color)
public TestPlan ExpectCanvasDrawFilledRectangle(Position position, Size size, string color)
{
return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
return AddOperation(new CanvasDrawFilledRectangleOperation(position, size, color));
}
public TestPlan ExpectCanvasDrawStrokedRectangle(Size size, string color, float width)
{
return AddOperation(new CanvasDrawStrokedRectangleOperation(size, color, width));
}
public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
+43 -11
View File
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing
@@ -17,6 +20,13 @@ namespace QuestPDF.Drawing
RenderDocument(canvas, document);
}
internal static void GenerateXps(Stream stream, IDocument document)
{
var metadata = document.GetMetadata();
var canvas = new XpsCanvas(stream, metadata);
RenderDocument(canvas, document);
}
internal static ICollection<byte[]> GenerateImages(IDocument document)
{
var metadata = document.GetMetadata();
@@ -36,16 +46,10 @@ namespace QuestPDF.Drawing
var metadata = document.GetMetadata();
var pageContext = new PageContext();
DebuggingState debuggingState = null;
if (System.Diagnostics.Debugger.IsAttached)
{
debuggingState = ApplyDebugging(content);
}
else
{
var debuggingState = metadata.ApplyDebugging ? ApplyDebugging(content) : null;
if (metadata.ApplyCaching)
ApplyCaching(content);
}
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
RenderPass(pageContext, canvas, content, metadata, debuggingState);
@@ -103,7 +107,13 @@ namespace QuestPDF.Drawing
void ThrowLayoutException()
{
throw new DocumentLayoutException("Composed layout generates infinite document.")
var message = $"Composed layout generates infinite document. This may happen in two cases. " +
$"1) Your document and its layout configuration is correct but the content takes more than {documentMetadata.DocumentLayoutExceptionThreshold} pages. " +
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
$"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.";
throw new DocumentLayoutException(message)
{
ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
};
@@ -125,10 +135,32 @@ namespace QuestPDF.Drawing
content.HandleVisitor(x =>
{
x.CreateProxy(y => new DebuggingProxy(debuggingState, x));
x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
});
return debuggingState;
}
internal static void ApplyDefaultTextStyle(this Element content, TextStyle documentDefaultTextStyle)
{
documentDefaultTextStyle.ApplyGlobalStyle(TextStyle.LibraryDefault);
content.HandleVisitor(element =>
{
var text = element as TextBlock;
if (text == null)
return;
foreach (var child in text.Children)
{
if (child is TextBlockSpan textSpan)
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
if (child is TextBlockElement textElement)
ApplyDefaultTextStyle(textElement.Element, documentDefaultTextStyle);
}
});
}
}
}
+4 -1
View File
@@ -23,7 +23,10 @@ namespace QuestPDF.Drawing
/// (likely due to infinite layout), the exception is thrown.
/// </summary>
public int DocumentLayoutExceptionThreshold { get; set; } = 250;
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
public static DocumentMetadata Default => new DocumentMetadata();
}
}
+5 -5
View File
@@ -33,7 +33,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ToPaint(this TextStyle style)
{
return Paints.GetOrAdd(style.ToString(), key => Convert(style));
return Paints.GetOrAdd(style.Key, key => Convert(style));
static SKPaint Convert(TextStyle style)
{
@@ -41,7 +41,7 @@ namespace QuestPDF.Drawing
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = style.Size,
TextSize = (style.Size ?? 12),
TextEncoding = SKTextEncoding.Utf32
};
}
@@ -51,16 +51,16 @@ namespace QuestPDF.Drawing
if (Typefaces.TryGetValue(style.FontType, out var result))
return result;
var slant = style.IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
return SKTypeface.FromFamilyName(style.FontType, (int)style.FontWeight, (int)SKFontStyleWidth.Normal, slant)
return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant)
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
}
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.ToString(), key => style.ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics);
}
}
}
+6 -1
View File
@@ -36,7 +36,12 @@ namespace QuestPDF.Drawing
}
public void DrawRectangle(Position vector, Size size, string color)
public void DrawFilledRectangle(Position vector, Size size, string color)
{
}
public void DrawStrokedRectangle(Size size, string color, float width)
{
}
+3 -33
View File
@@ -4,43 +4,13 @@ using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class PdfCanvas : SkiaCanvasBase
internal class PdfCanvas : SkiaDocumentCanvasBase
{
private SKDocument Document { get; }
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
{
Document = SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
}
~PdfCanvas()
{
Document.Dispose();
}
public override void BeginDocument()
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)))
{
}
public override void EndDocument()
{
Canvas?.Dispose();
Document?.Close();
Document?.Dispose();
}
public override void BeginPage(Size size)
{
Canvas = Document.BeginPage(size.Width, size.Height);
}
public override void EndPage()
{
Document.EndPage();
Canvas.Dispose();
}
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
{
+2 -2
View File
@@ -76,7 +76,7 @@ namespace QuestPDF.Drawing.Proxy
title += debugPointer.Highlight ? " 🌟" : string.Empty;
}
if (item.SpacePlan.Type == SpacePlanType.Wrap)
if (item.SpacePlan.Type != SpacePlanType.FullRender)
title = "🔥 " + title;
builder.AppendLine(indent + title);
@@ -109,7 +109,7 @@ namespace QuestPDF.Drawing.Proxy
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => !(x.Value is IEnumerable<IElement>))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
+41
View File
@@ -0,0 +1,41 @@
using System.Collections.Concurrent;
using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class SkiaCache
{
private static ConcurrentDictionary<string, SKPaint> RectangleFillPaints = new ConcurrentDictionary<string, SKPaint>();
private static ConcurrentDictionary<string, SKPaint> RectangleStrokePaints = new ConcurrentDictionary<string, SKPaint>();
public static SKPaint GetRectangleFillPaint(string color)
{
return RectangleFillPaints.GetOrAdd(color, _ => Convert());
SKPaint Convert()
{
return new SKPaint
{
Color = SKColor.Parse(color),
Style = SKPaintStyle.Fill
};
}
}
public static SKPaint GetRectangleStrokePaint(string color, float width)
{
var key = $"{color}|{width}";
return RectangleStrokePaints.GetOrAdd(key, _ => Convert());
SKPaint Convert()
{
return new SKPaint
{
Color = SKColor.Parse(color),
Style = SKPaintStyle.Stroke,
StrokeWidth = width
};
}
}
}
}
+8 -2
View File
@@ -18,14 +18,20 @@ namespace QuestPDF.Drawing
Canvas.Translate(vector.X, vector.Y);
}
public void DrawRectangle(Position vector, Size size, string color)
public void DrawFilledRectangle(Position vector, Size size, string color)
{
if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
return;
var paint = color.ColorToPaint();
var paint = SkiaCache.GetRectangleFillPaint(color);
Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
}
public void DrawStrokedRectangle(Size size, string color, float width)
{
var paint = SkiaCache.GetRectangleStrokePaint(color, width);
Canvas.DrawRect(0, 0, size.Width, size.Height, paint);
}
public void DrawText(string text, Position vector, TextStyle style)
{
@@ -0,0 +1,45 @@
using System.IO;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class SkiaDocumentCanvasBase : SkiaCanvasBase
{
private SKDocument Document { get; }
public SkiaDocumentCanvasBase(SKDocument document)
{
Document = document;
}
~SkiaDocumentCanvasBase()
{
Document.Dispose();
}
public override void BeginDocument()
{
}
public override void EndDocument()
{
Canvas.Dispose();
Document.Close();
Document.Dispose();
}
public override void BeginPage(Size size)
{
Canvas = Document.BeginPage(size.Width, size.Height);
}
public override void EndPage()
{
Document.EndPage();
Canvas.Dispose();
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ namespace QuestPDF.Drawing
if (Type == SpacePlanType.Wrap)
return Type.ToString();
return $"{Type} | width {Width:N2} | height {Height:N2}";
return $"{Type} (Width: {Width:N2}, Height: {Height:N2})";
}
public static implicit operator Size(SpacePlan spacePlan)
+15
View File
@@ -0,0 +1,15 @@
using System.IO;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Drawing
{
internal class XpsCanvas : SkiaDocumentCanvasBase
{
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi))
{
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
Canvas.DrawRectangle(Position.Zero, availableSpace, Color);
Canvas.DrawFilledRectangle(Position.Zero, availableSpace, Color);
base.Draw(availableSpace);
}
}
+42 -22
View File
@@ -1,3 +1,4 @@
using System;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
@@ -15,31 +16,50 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
base.Draw(availableSpace);
Canvas.DrawRectangle(
new Position(-Left/2, -Top/2),
new Size(availableSpace.Width + Left/2 + Right/2, Top),
Color);
Canvas.DrawRectangle(
new Position(-Left/2, -Top/2),
new Size(Left, availableSpace.Height + Top/2 + Bottom/2),
Color);
Canvas.DrawRectangle(
new Position(-Left/2, availableSpace.Height-Bottom/2),
new Size(availableSpace.Width + Left/2 + Right/2, Bottom),
Color);
Canvas.DrawRectangle(
new Position(availableSpace.Width-Right/2, -Top/2),
new Size(Right, availableSpace.Height + Top/2 + Bottom/2),
Color);
if (HasEqualWidthOnAllSides())
{
Canvas.DrawStrokedRectangle(availableSpace, Color, Top);
}
else
{
DrawCustomRectangle(availableSpace);
}
}
public override string ToString()
private void DrawCustomRectangle(Size size)
{
return $"Border: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left}) Color({Color})";
Canvas.DrawFilledRectangle(
new Position(-Left / 2, -Top / 2),
new Size(size.Width + Left / 2 + Right / 2, Top),
Color);
Canvas.DrawFilledRectangle(
new Position(-Left / 2, -Top / 2),
new Size(Left, size.Height + Top / 2 + Bottom / 2),
Color);
Canvas.DrawFilledRectangle(
new Position(-Left / 2, size.Height - Bottom / 2),
new Size(size.Width + Left / 2 + Right / 2, Bottom),
Color);
Canvas.DrawFilledRectangle(
new Position(size.Width - Right / 2, -Top / 2),
new Size(Right, size.Height + Top / 2 + Bottom / 2),
Color);
}
private bool HasEqualWidthOnAllSides()
{
return IsClose(Top, Bottom) &&
IsClose(Left, Right) &&
IsClose(Top, Left);
}
private static bool IsClose(float x, float y)
{
return Math.Abs(x - y) < Size.Epsilon;
}
}
}
+2 -2
View File
@@ -22,9 +22,9 @@ namespace QuestPDF.Elements
if (Handler == null || skiaCanvas == null)
return;
var currentMatrix = skiaCanvas.TotalMatrix;
var originalMatrix = skiaCanvas.TotalMatrix;
Handler.Invoke(skiaCanvas, availableSpace);
skiaCanvas.SetMatrix(currentMatrix);
skiaCanvas.SetMatrix(originalMatrix);
}
}
}
+14 -14
View File
@@ -22,8 +22,8 @@ namespace QuestPDF.Elements
return SpacePlan.Wrap();
var available = new Size(
LimitMin(availableSpace.Width, MaxWidth),
LimitMin(availableSpace.Height, MaxHeight));
Min(MaxWidth, availableSpace.Width),
Min(MaxHeight, availableSpace.Height));
var measurement = base.Measure(available);
@@ -31,8 +31,8 @@ namespace QuestPDF.Elements
return SpacePlan.Wrap();
var actualSize = new Size(
LimitMax(measurement.Width, MinWidth),
LimitMax(measurement.Height, MinHeight));
Max(MinWidth, measurement.Width),
Max(MinHeight, measurement.Height));
if (measurement.Type == SpacePlanType.FullRender)
return SpacePlan.FullRender(actualSize);
@@ -46,20 +46,20 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
var available = new Size(
LimitMin(availableSpace.Width, MaxWidth),
LimitMin(availableSpace.Height, MaxHeight));
Min(MaxWidth, availableSpace.Width),
Min(MaxHeight, availableSpace.Height));
base.Draw(available);
}
private float LimitMin(float value, float? limit)
{
return limit.HasValue ? Math.Min(value, limit.Value) : value;
Child?.Draw(available);
}
private float LimitMax(float value, float? limit)
private static float Min(float? x, float y)
{
return limit.HasValue ? Math.Max(value, limit.Value) : value;
return x.HasValue ? Math.Min(x.Value, y) : y;
}
private static float Max(float? x, float y)
{
return x.HasValue ? Math.Max(x.Value, y) : y;
}
}
}
+4 -5
View File
@@ -2,7 +2,6 @@
using System.Linq;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using static QuestPDF.Infrastructure.HorizontalAlignment;
namespace QuestPDF.Elements
{
@@ -20,7 +19,7 @@ namespace QuestPDF.Elements
public Queue<GridElement> ChildrenQueue { get; set; } = new Queue<GridElement>();
public int ColumnsCount { get; set; } = DefaultColumnsCount;
public HorizontalAlignment Alignment { get; set; } = Left;
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public float VerticalSpacing { get; set; } = 0;
public float HorizontalSpacing { get; set; } = 0;
@@ -62,15 +61,15 @@ namespace QuestPDF.Elements
var emptySpace = ColumnsCount - columnsWidth;
var hasEmptySpace = emptySpace >= Size.Epsilon;
if (Alignment == Center)
if (Alignment == HorizontalAlignment.Center)
emptySpace /= 2;
if (hasEmptySpace && Alignment != Left)
if (hasEmptySpace && Alignment != HorizontalAlignment.Left)
row.RelativeColumn(emptySpace);
elements.ForEach(x => row.RelativeColumn(x.Columns).Element(x.Child));
if (hasEmptySpace && Alignment != Right)
if (hasEmptySpace && Alignment != HorizontalAlignment.Right)
row.RelativeColumn(emptySpace);
}
}
+262
View File
@@ -0,0 +1,262 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class InlinedElement : Container
{
public SpacePlan? MeasureCache { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
// TODO: once element caching proxy is introduces, this can be removed
MeasureCache ??= Child.Measure(Size.Max);
return MeasureCache.Value;
}
}
internal enum InlinedAlignment
{
Left,
Center,
Right,
Justify,
SpaceAround
}
internal class Inlined : Element, IStateResettable
{
public List<InlinedElement> Elements { get; internal set; } = new List<InlinedElement>();
private Queue<InlinedElement> ChildrenQueue { get; set; }
internal float VerticalSpacing { get; set; }
internal float HorizontalSpacing { get; set; }
internal InlinedAlignment ElementsAlignment { get; set; }
internal VerticalAlignment BaselineAlignment { get; set; }
public void ResetState()
{
ChildrenQueue = new Queue<InlinedElement>(Elements);
}
internal override void HandleVisitor(Action<Element?> visit)
{
Elements.ForEach(x => x.HandleVisitor(visit));
base.HandleVisitor(visit);
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!ChildrenQueue.Any())
return SpacePlan.FullRender(Size.Zero);
var lines = Compose(availableSpace);
if (!lines.Any())
return SpacePlan.Wrap();
var lineSizes = lines
.Select(line =>
{
var size = GetLineSize(line);
var widthWithSpacing = size.Width + (line.Count - 1) * HorizontalSpacing;
return new Size(widthWithSpacing, size.Height);
})
.ToList();
var width = lineSizes.Max(x => x.Width);
var height = lineSizes.Sum(x => x.Height) + (lines.Count - 1) * VerticalSpacing;
var targetSize = new Size(width, height);
var isPartiallyRendered = lines.Sum(x => x.Count) != ChildrenQueue.Count;
if (isPartiallyRendered)
return SpacePlan.PartialRender(targetSize);
return SpacePlan.FullRender(targetSize);
}
internal override void Draw(Size availableSpace)
{
var lines = Compose(availableSpace);
var topOffset = 0f;
foreach (var line in lines)
{
var height = line
.Select(x => x.Measure(Size.Max))
.Where(x => x.Type != SpacePlanType.Wrap)
.Max(x => x.Height);
DrawLine(line);
topOffset += height + VerticalSpacing;
Canvas.Translate(new Position(0, height + VerticalSpacing));
}
Canvas.Translate(new Position(0, -topOffset));
lines.SelectMany(x => x).ToList().ForEach(x => ChildrenQueue.Dequeue());
void DrawLine(ICollection<InlinedElement> elements)
{
var lineSize = GetLineSize(elements);
var elementOffset = ElementOffset();
var leftOffset = AlignOffset();
Canvas.Translate(new Position(leftOffset, 0));
foreach (var element in elements)
{
var size = element.Measure(Size.Max);
var baselineOffset = BaselineOffset(size, lineSize.Height);
Canvas.Translate(new Position(0, baselineOffset));
element.Draw(size);
Canvas.Translate(new Position(0, -baselineOffset));
leftOffset += size.Width + elementOffset;
Canvas.Translate(new Position(size.Width + elementOffset, 0));
}
Canvas.Translate(new Position(-leftOffset, 0));
float ElementOffset()
{
var difference = availableSpace.Width - lineSize.Width;
if (elements.Count == 1)
return 0;
if (ElementsAlignment == InlinedAlignment.Justify)
return difference / (elements.Count - 1);
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return difference / (elements.Count + 1);
return HorizontalSpacing;
}
float AlignOffset()
{
if (ElementsAlignment == InlinedAlignment.Left)
return 0;
if (ElementsAlignment == InlinedAlignment.Justify)
return 0;
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return elementOffset;
var difference = availableSpace.Width - lineSize.Width - (elements.Count - 1) * HorizontalSpacing;
if (ElementsAlignment == InlinedAlignment.Center)
return difference / 2;
if (ElementsAlignment == InlinedAlignment.Right)
return difference;
return 0;
}
float BaselineOffset(Size elementSize, float lineHeight)
{
if (BaselineAlignment == VerticalAlignment.Top)
return 0;
var difference = lineHeight - elementSize.Height;
if (BaselineAlignment == VerticalAlignment.Middle)
return difference / 2;
return difference;
}
}
}
Size GetLineSize(ICollection<InlinedElement> elements)
{
var sizes = elements
.Select(x => x.Measure(Size.Max))
.Where(x => x.Type != SpacePlanType.Wrap)
.ToList();
var width = sizes.Sum(x => x.Width);
var height = sizes.Max(x => x.Height);
return new Size(width, height);
}
// list of lines, each line is a list of elements
private ICollection<ICollection<InlinedElement>> Compose(Size availableSize)
{
var queue = new Queue<InlinedElement>(ChildrenQueue);
var result = new List<ICollection<InlinedElement>>();
var topOffset = 0f;
while (true)
{
var line = GetNextLine();
if (!line.Any())
break;
var height = line
.Select(x => x.Measure(availableSize))
.Where(x => x.Type != SpacePlanType.Wrap)
.Max(x => x.Height);
if (topOffset + height > availableSize.Height + Size.Epsilon)
break;
topOffset += height + VerticalSpacing;
result.Add(line);
}
return result;
ICollection<InlinedElement> GetNextLine()
{
var result = new List<InlinedElement>();
var leftOffset = GetInitialAlignmentOffset();
while (true)
{
if (!queue.Any())
break;
var element = queue.Peek();
var size = element.Measure(Size.Max);
if (size.Type == SpacePlanType.Wrap)
break;
if (leftOffset + size.Width > availableSize.Width + Size.Epsilon)
break;
queue.Dequeue();
leftOffset += size.Width + HorizontalSpacing;
result.Add(element);
}
return result;
}
float GetInitialAlignmentOffset()
{
// this method makes sure that the spacing between elements is no lesser than configured
if (ElementsAlignment == InlinedAlignment.SpaceAround)
return HorizontalSpacing * 2;
return 0;
}
}
}
}
+9 -1
View File
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
@@ -7,6 +8,8 @@ namespace QuestPDF.Elements
{
internal class Page : IComponent
{
public TextStyle DefaultTextStyle { get; set; } = new TextStyle();
public Size MinSize { get; set; } = PageSizes.A4;
public Size MaxSize { get; set; } = PageSizes.A4;
@@ -15,6 +18,8 @@ namespace QuestPDF.Elements
public float MarginTop { get; set; }
public float MarginBottom { get; set; }
public string BackgroundColor { get; set; } = Colors.Transparent;
public Element Header { get; set; } = Empty.Instance;
public Element Content { get; set; } = Empty.Instance;
public Element Footer { get; set; } = Empty.Instance;
@@ -22,12 +27,13 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.MinWidth(MinSize.Width)
.MinHeight(MinSize.Height)
.MaxWidth(MaxSize.Width)
.MaxHeight(MaxSize.Height)
.Background(BackgroundColor)
.PaddingLeft(MarginLeft)
.PaddingRight(MarginRight)
@@ -54,6 +60,8 @@ namespace QuestPDF.Elements
.Element(Footer);
});
(container as Element).ApplyDefaultTextStyle(DefaultTextStyle);
bool IsClose(float x, float y)
{
return Math.Abs(x - y) < Size.Epsilon;
+34
View File
@@ -0,0 +1,34 @@
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class SkipOnce : ContainerElement, IStateResettable
{
private bool FirstPageWasSkipped { get; set; }
public void ResetState()
{
FirstPageWasSkipped = false;
}
internal override SpacePlan Measure(Size availableSpace)
{
if (Child == null || !FirstPageWasSkipped)
return SpacePlan.FullRender(Size.Zero);
return Child.Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
if (Child == null)
return;
if (FirstPageWasSkipped)
Child.Draw(availableSpace);
FirstPageWasSkipped = true;
}
}
}
@@ -1,4 +1,5 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
@@ -3,33 +3,22 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockExternalLink : ITextBlockItem
internal class TextBlockExternalLink : TextBlockSpan
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string Url { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
return MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextBlockSpan GetItem()
{
return new TextBlockSpan
{
Style = Style,
Text = Text
};
base.Draw(request);
}
}
}
@@ -3,33 +3,22 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockInternalLink : ITextBlockItem
internal class TextBlockInternalLink : TextBlockSpan
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string LocationName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
return MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextBlockSpan GetItem()
{
return new TextBlockSpan
{
Style = Style,
Text = Text
};
base.Draw(request);
}
}
}
@@ -3,34 +3,31 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockPageNumber : ITextBlockItem
internal class TextBlockPageNumber : TextBlockSpan
{
public TextStyle Style { get; set; } = new TextStyle();
public string SlotName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem(request.PageContext).MeasureWithoutCache(request);
SetPageNumber(request.PageContext);
return MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
public override void Draw(TextDrawingRequest request)
{
GetItem(request.PageContext).Draw(request);
SetPageNumber(request.PageContext);
base.Draw(request);
}
private TextBlockSpan GetItem(IPageContext context)
private void SetPageNumber(IPageContext context)
{
var pageNumberPlaceholder = 123;
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
? context.GetLocationPage(SlotName)
: pageNumberPlaceholder;
return new TextBlockSpan
{
Style = Style,
Text = pageNumber.ToString()
};
Text = pageNumber.ToString();
}
}
}
+10 -10
View File
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements.Text.Items
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
public TextMeasurementResult? Measure(TextMeasurementRequest request)
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
@@ -45,7 +45,7 @@ namespace QuestPDF.Elements.Text.Items
{
Width = 0,
LineHeight = Style.LineHeight,
LineHeight = Style.LineHeight ?? 1,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent
};
@@ -96,7 +96,7 @@ namespace QuestPDF.Elements.Text.Items
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight,
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
@@ -105,26 +105,26 @@ namespace QuestPDF.Elements.Text.Items
};
}
public void Draw(TextDrawingRequest request)
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawFilledRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, Position.Zero, Style);
// draw underline
if (Style.HasUnderline && fontMetrics.UnderlinePosition.HasValue)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1);
// draw stroke
if (Style.HasStrikethrough && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value);
if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1);
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
request.Canvas.DrawFilledRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
+4 -2
View File
@@ -13,8 +13,10 @@ namespace QuestPDF.Elements.Text
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Children { get; set; } = new List<ITextBlockItem>();
public Queue<ITextBlockItem> RenderingQueue { get; set; }
public int CurrentElementIndex { get; set; }
public string Text => string.Join(" ", Children.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
public void ResetState()
{
+6 -1
View File
@@ -70,7 +70,12 @@ namespace QuestPDF.Fluent
{
return element.Element(new ShowOnce());
}
public static IContainer SkipOnce(this IContainer element)
{
return element.Element(new SkipOnce());
}
public static IContainer ShowEntire(this IContainer element)
{
return element.Element(new ShowEntire());
+30
View File
@@ -8,6 +8,8 @@ namespace QuestPDF.Fluent
{
public static class GenerateExtensions
{
#region PDF
public static byte[] GeneratePdf(this IDocument document)
{
using var stream = new MemoryStream();
@@ -26,6 +28,32 @@ namespace QuestPDF.Fluent
DocumentGenerator.GeneratePdf(stream, document);
}
#endregion
public static byte[] GenerateXps(this IDocument document)
{
using var stream = new MemoryStream();
document.GenerateXps(stream);
return stream.ToArray();
}
public static void GenerateXps(this IDocument document, string filePath)
{
using var stream = new FileStream(filePath, FileMode.Create);
document.GenerateXps(stream);
}
public static void GenerateXps(this IDocument document, Stream stream)
{
DocumentGenerator.GenerateXps(stream, document);
}
#region XPS
#endregion
#region Images
public static IEnumerable<byte[]> GenerateImages(this IDocument document)
{
return DocumentGenerator.GenerateImages(document);
@@ -47,5 +75,7 @@ namespace QuestPDF.Fluent
index++;
}
}
#endregion
}
}
+23 -4
View File
@@ -1,4 +1,6 @@
using System;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -7,12 +9,29 @@ namespace QuestPDF.Fluent
{
public static class ImageExtensions
{
public static void Image(this IContainer parent, byte[] data, ImageScaling scaling = ImageScaling.FitWidth)
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
{
if (data == null)
return;
var image = SKImage.FromEncodedData(imageData);
parent.Image(image, scaling);
}
public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
{
var image = SKImage.FromEncodedData(filePath);
parent.Image(image, scaling);
}
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
{
var image = SKImage.FromEncodedData(fileStream);
parent.Image(image, scaling);
}
private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth)
{
if (image == null)
throw new DocumentComposeException("Cannot load or decode provided image.");
var image = SKImage.FromEncodedData(data);
var aspectRatio = image.Width / (float)image.Height;
var imageElement = new Image
+50
View File
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class InlinedDescriptor
{
internal Inlined Inlined { get; } = new Inlined();
public void Spacing(float value)
{
VerticalSpacing(value);
HorizontalSpacing(value);
}
public void VerticalSpacing(float value) => Inlined.VerticalSpacing = value;
public void HorizontalSpacing(float value) => Inlined.HorizontalSpacing = value;
public void BaselineTop() => Inlined.BaselineAlignment = VerticalAlignment.Top;
public void BaselineMiddle() => Inlined.BaselineAlignment = VerticalAlignment.Middle;
public void BaselineBottom() => Inlined.BaselineAlignment = VerticalAlignment.Bottom;
public void AlignLeft() => Inlined.ElementsAlignment = InlinedAlignment.Left;
public void AlignCenter() => Inlined.ElementsAlignment = InlinedAlignment.Center;
public void AlignRight() => Inlined.ElementsAlignment = InlinedAlignment.Right;
public void AlignJustify() => Inlined.ElementsAlignment = InlinedAlignment.Justify;
public void AlignSpaceAround() => Inlined.ElementsAlignment = InlinedAlignment.SpaceAround;
public IContainer Item()
{
var container = new InlinedElement();
Inlined.Elements.Add(container);
return container;
}
}
public static class InlinedExtensions
{
public static void Inlined(this IContainer element, Action<InlinedDescriptor> handler)
{
var descriptor = new InlinedDescriptor();
handler(descriptor);
element.Element(descriptor.Inlined);
}
}
}
+10
View File
@@ -70,6 +70,16 @@ namespace QuestPDF.Fluent
MarginHorizontal(value);
}
public void DefaultTextStyle(TextStyle textStyle)
{
Page.DefaultTextStyle = textStyle;
}
public void Background(string color)
{
Page.BackgroundColor = color;
}
public IContainer Header()
{
var container = new Container();
+42 -22
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
@@ -13,7 +14,7 @@ namespace QuestPDF.Fluent
{
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
private HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
private float Spacing { get; set; } = 0f;
public void DefaultTextStyle(TextStyle style)
@@ -49,9 +50,12 @@ namespace QuestPDF.Fluent
TextBlocks.Last().Children.Add(item);
}
public void Span(string text, TextStyle? style = null)
public void Span(string? text, TextStyle? style = null)
{
style ??= DefaultStyle;
if (IsNullOrEmpty(text))
return;
style ??= TextStyle.Default;
var items = text
.Replace("\r", string.Empty)
@@ -75,16 +79,12 @@ namespace QuestPDF.Fluent
.ForEach(TextBlocks.Add);
}
public void Line(string text, TextStyle? style = null)
public void Line(string? text, TextStyle? style = null)
{
text ??= string.Empty;
Span(text + Environment.NewLine, style);
}
public void Line(string text)
{
Span(text + Environment.NewLine);
}
public void EmptyLine()
{
Span(Environment.NewLine);
@@ -92,7 +92,10 @@ namespace QuestPDF.Fluent
private void PageNumber(string slotName, TextStyle? style = null)
{
style ??= DefaultStyle;
if (IsNullOrEmpty(slotName))
throw new ArgumentException(nameof(slotName));
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockPageNumber()
{
@@ -113,12 +116,21 @@ namespace QuestPDF.Fluent
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
{
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
PageNumber(locationName, style);
}
public void InternalLocation(string text, string locationName, TextStyle? style = null)
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
{
style ??= DefaultStyle;
if (IsNullOrEmpty(text))
return;
if (IsNullOrEmpty(locationName))
throw new ArgumentException(nameof(locationName));
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockInternalLink
{
@@ -128,9 +140,15 @@ namespace QuestPDF.Fluent
});
}
public void ExternalLocation(string text, string url, TextStyle? style = null)
public void ExternalLocation(string? text, string url, TextStyle? style = null)
{
style ??= DefaultStyle;
if (IsNullOrEmpty(text))
return;
if (IsNullOrEmpty(url))
throw new ArgumentException(nameof(url));
style ??= TextStyle.Default;
AddItemToLastTextBlock(new TextBlockExternalLink
{
@@ -156,6 +174,9 @@ namespace QuestPDF.Fluent
{
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
foreach (var textBlockSpan in TextBlocks.SelectMany(x => x.Children).Where(x => x is TextBlockSpan).Cast<TextBlockSpan>())
textBlockSpan.Style.ApplyParentStyle(DefaultStyle);
container.Stack(stack =>
{
stack.Spacing(Spacing);
@@ -170,19 +191,18 @@ namespace QuestPDF.Fluent
{
public static void Text(this IContainer element, Action<TextDescriptor> content)
{
var textBlock = new TextBlock();
if (element is Alignment alignment)
textBlock.Alignment = alignment.Horizontal;
var descriptor = new TextDescriptor();
if (element is Alignment alignment)
descriptor.Alignment = alignment.Horizontal;
content?.Invoke(descriptor);
descriptor.Compose(element);
}
public static void Text(this IContainer element, object text, TextStyle? style = null)
public static void Text(this IContainer element, object? text, TextStyle? style = null)
{
element.Text(x => x.Span(text.ToString(), style));
element.Text(x => x.Span(text?.ToString(), style));
}
}
}
+2 -2
View File
@@ -65,12 +65,12 @@ namespace QuestPDF.Helpers
public static class PageSizeExtensions
{
public static PageSize Portrait(this Size size)
public static PageSize Portrait(this PageSize size)
{
return new PageSize(Math.Min(size.Width, size.Height), Math.Max(size.Width, size.Height));
}
public static PageSize Landscape(this Size size)
public static PageSize Landscape(this PageSize size)
{
return new PageSize(Math.Max(size.Width, size.Height), Math.Min(size.Width, size.Height));
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
{
public static class Placeholders
{
private static Random Random = new Random();
public static readonly Random Random = new Random();
#region Word Cache
+2 -1
View File
@@ -6,7 +6,8 @@ namespace QuestPDF.Infrastructure
{
void Translate(Position vector);
void DrawRectangle(Position vector, Size size, string color);
void DrawFilledRectangle(Position vector, Size size, string color);
void DrawStrokedRectangle(Size size, string color, float width);
void DrawText(string text, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
+3 -3
View File
@@ -7,8 +7,8 @@
public readonly float Width;
public readonly float Height;
public static Size Zero => new Size(0, 0);
public static Size Max => new Size(14_400, 14_400);
public static Size Zero { get; } = new Size(0, 0);
public static Size Max { get; } = new Size(14_400, 14_400);
public Size(float width, float height)
{
@@ -16,6 +16,6 @@
Height = height;
}
public override string ToString() => $"{Width:N2} {Height:N2}";
public override string ToString() => $"(Width: {Width}, Height: {Height})";
}
}
+55 -16
View File
@@ -1,29 +1,68 @@
using QuestPDF.Helpers;
using System;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
public class TextStyle
{
internal string Color { get; set; } = Colors.Black;
internal string BackgroundColor { get; set; } = Colors.Transparent;
internal string FontType { get; set; } = "Calibri";
internal float Size { get; set; } = 12;
internal float LineHeight { get; set; } = 1.2f;
internal FontWeight FontWeight { get; set; } = FontWeight.Normal;
internal bool IsItalic { get; set; } = false;
internal bool HasStrikethrough { get; set; } = false;
internal bool HasUnderline { get; set; } = false;
internal bool HasGlobalStyleApplied { get; private set; }
internal string? Color { get; set; }
internal string? BackgroundColor { get; set; }
internal string? FontType { get; set; }
internal float? Size { get; set; }
internal float? LineHeight { get; set; }
internal FontWeight? FontWeight { get; set; }
internal bool? IsItalic { get; set; }
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal string? Key { get; private set; }
internal static TextStyle LibraryDefault => new TextStyle
{
Color = Colors.Black,
BackgroundColor = Colors.Transparent,
FontType = Fonts.Calibri,
Size = 12,
LineHeight = 1.2f,
FontWeight = Infrastructure.FontWeight.Normal,
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false
};
public static TextStyle Default => new TextStyle();
private string? KeyCache { get; set; }
public override string ToString()
internal void ApplyGlobalStyle(TextStyle globalStyle)
{
KeyCache ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
return KeyCache;
if (HasGlobalStyleApplied)
return;
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
}
internal void ApplyParentStyle(TextStyle parentStyle)
{
Color ??= parentStyle.Color;
BackgroundColor ??= parentStyle.BackgroundColor;
FontType ??= parentStyle.FontType;
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
}
internal TextStyle Clone() => (TextStyle)MemberwiseClone();
internal TextStyle Clone()
{
var clone = (TextStyle)MemberwiseClone();
clone.HasGlobalStyleApplied = false;
return clone;
}
}
}
+5 -3
View File
@@ -4,9 +4,9 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2021.10.1</Version>
<Version>2021.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>Enhanced text rendering capabilities. Improved rendering performance.</PackageReleaseNotes>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>8</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon>
@@ -15,10 +15,12 @@
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
<PackageTags>pdf file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
<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</TargetFrameworks>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
+208 -14
View File
@@ -1,27 +1,221 @@
## Rely on solid fundamentals
## QuestPDF Overview
This library is created specifically for designing and arranging document layouts, with full paging support. Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
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.
## Work with organized self-explanatory code
## Features
The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
**Rely on solid fundamentals** - This library is created specifically for designing and arranging document layouts, with full paging support. Alternative solutions, such as HTML-based converters, are not designed for this purpose and therefore are often unpredictable and do not produce desired results.
## Compose simple components into complex documents
**Work with organized self-explanatory code** - The entire process of implementing PDF document, takes place in your code. Free yourself from slow visual designers and strange technological limitations. Follow simple yet highly effective approaches to create maintainable, high-quality code.
Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
**Compose simple components into complex documents** - Do you remember the feeling when your code just works? When your ideas are becoming real without any effort? Working with simple, easy to understand, self-explanatory and highly composable layout elements is the key here!
## Create and reuse components
**Create and reuse components** - Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
Feel no fear of complex documents! Create custom, reusable components and divide the document's layout into easy to maintain pieces. Inject data to customize content and use slots to enhance composability. Decide how complex approaches your solution needs and follow the best path.
**Prototype with ease** - We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
## Prototype with ease
**Enjoy fast PDF generation** - QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
We understand that document generation is often tricky and require multiple iterations. The library offers additional prototyping tools such as random text generator or image placeholder element. By following best practices, you can develop a document without having data.
## Learning resources
## Enjoy fast PDF generation
**[Release notes and roadmap](https://www.questpdf.com/documentation/releases.html)** - everything that is planned for future library iterations, description of new features and information about potential breaking changes.
QuestPDF is created upon SkiaSharp, a well-known graphical library, and converts your data into PDF documents. It offers a highly optimized layouting engine capable of generating over 1000 PDF files per minute per core. The entire process is thread-safe.
**[Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html)** - a short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
## Write less, achieve more
**[API Reference](https://www.questpdf.com/documentation/api-reference.html)** - a detailed description of behavior of all available components and how to use them with C# Fluent API.
Do you believe that creating a complete invoice document can take less than 200 lines of code? We have prepared for you a step-by-step instruction that shows every detail of this implementation and describes the best patterns and practices. [Learn more](https://www.questpdf.com/documentation/getting-started.html)
**[Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)** - everything that may help you designing great reports and reusable code that is easy to maintain.
## Example invoice
Do you believe that creating a complete invoice document can take less than 200 lines of code? We have prepared for you a step-by-step instruction that shows every detail of this implementation and describes the best patterns and practices.
For tutorial, documentation and API reference, please visit [the QuestPDF documentation](https://www.questpdf.com/documentation/getting-started.html).
![invoice](https://raw.githubusercontent.com/QuestPDF/example-invoice/main/images/invoice.png)
Here you can find an example code showing how easy is to write and understand the fluent API.
**General document structure** with header, content and footer:
```csharp
public void Compose(IDocumentContainer container)
{
container
.Page(page =>
{
page.Margin(50);
page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().AlignCenter().Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
});
}
```
**The header area** consists of basic invoice information along with a logo placeholder.
```csharp
void ComposeHeader(IContainer container)
{
var titleTextStyle = TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium);
container.Row(row =>
{
{
stack.Item().Text($"Invoice #{Model.InvoiceNumber}", titleStyle);
stack.Item().Text(text =>
{
text.Span("Issue date: ", TextStyle.Default.SemiBold());
text.Span($"{Model.IssueDate:d}");
});
stack.Item().Text(text =>
{
text.Span("Due date: ", TextStyle.Default.SemiBold());
text.Span($"{Model.DueDate:d}");
});
});
row.ConstantColumn(100).Height(50).Placeholder();
});
}
```
Implementation of **the content area** that contains seller and customer details, then listing of all bought products, then a comments section.
```csharp
void ComposeContent(IContainer container)
{
container.PaddingVertical(40).Stack(column =>
{
column.Spacing(20);
column.Item().Row(row =>
{
row.RelativeColumn().Component(new AddressComponent("From", Model.SellerAddress));
row.ConstantColumn(50);
row.RelativeColumn().Component(new AddressComponent("For", Model.CustomerAddress));
});
column.Item().Element(ComposeTable);
var totalPrice = Model.Items.Sum(x => x.Price * x.Quantity);
column
.Item()
.PaddingRight(5)
.AlignRight()
.Text($"Grand total: {totalPrice}$", TextStyle.Default.SemiBold());
if (!string.IsNullOrWhiteSpace(Model.Comments))
column.Item().PaddingTop(25).Element(ComposeComments);
});
}
```
**The table and comments** codes are extracted into separate methods to increase clarity:
```csharp
void ComposeTable(IContainer container)
{
var headerStyle = TextStyle.Default.SemiBold();
container.Decoration(decoration =>
{
// header
decoration.Header().BorderBottom(1).Padding(5).Row(row =>
{
row.ConstantColumn(25).Text("#", headerStyle);
row.RelativeColumn(3).Text("Product", headerStyle);
row.RelativeColumn().AlignRight().Text("Unit price", headerStyle);
row.RelativeColumn().AlignRight().Text("Quantity", headerStyle);
row.RelativeColumn().AlignRight().Text("Total", headerStyle);
});
// content
decoration
.Content()
.Stack(column =>
{
foreach (var item in Model.Items)
{
column
.Item()
.ShowEntire()
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.Padding(5)
.Row(row =>
{
row.ConstantColumn(25).Text(Model.Items.IndexOf(item) + 1);
row.RelativeColumn(3).Text(item.Name);
row.RelativeColumn().AlignRight().Text($"{item.Price}$");
row.RelativeColumn().AlignRight().Text(item.Quantity);
row.RelativeColumn().AlignRight().Text($"{item.Price * item.Quantity}$");
});
}
});
});
}
```
```csharp
void ComposeComments(IContainer container)
{
container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Stack(message =>
{
message.Spacing(5);
message.Item().Text("Comments", TextStyle.Default.Size(14).SemiBold());
message.Item().Text(Model.Comments);
});
}
```
**The address details section** is implemented using components. This way the code can be easily reused for both seller and customer:
```csharp
public class AddressComponent : IComponent
{
private string Title { get; }
private Address Address { get; }
public AddressComponent(string title, Address address)
{
Title = title;
Address = address;
}
public void Compose(IContainer container)
{
container.ShowEntire().Stack(column =>
{
column.Spacing(5);
column
.Item()
.BorderBottom(1)
.PaddingBottom(5)
.Text(Title, TextStyle.Default.SemiBold());
column.Item().Text(Address.CompanyName);
column.Item().Text(Address.Street);
column.Item().Text($"{Address.City}, {Address.State}");
column.Item().Text(Address.Email);
column.Item().Text(Address.Phone);
});
}
}
```
+4
View File
@@ -0,0 +1,4 @@
- Improved debugging experience for layout-related exceptions. To make the library predictable, it is (by design) very strict about layouting rules and throws an exception when a constraint cannot be met. In this release, each exception contains an element stack that contains all information needed to identify the issue. By default, this feature is enabled only when debugger is attached.
- Improved layouting algorithm performance by introducing additional caching layer. This cache reduces the layouting time by half. By default, this feature is enabled only when debugger is not attached (mostly release mode).
- Reduced GA pressure put by the layouting algorithm. Previously, every element measurement operation was represented by an object and the paging support was done via class hierarchy. New solution uses structs (which are value-types) and enums. This also makes the code more readable and easier to follow.
- Added support for generating XPS files which are easier to print in the Windows environment. This was possible due to existing support in SkiaSharp. This change was proposed by **sbrkich**, thank you!
+3
View File
@@ -37,8 +37,11 @@ The library is available as a nuget package. You can install it as any other nug
## Documentation
**[Release notes and roadmap](https://www.questpdf.com/documentation/releases.html)** - everything that is planned for future library iterations, description of new features and information about potential breaking changes.
**[Getting started tutorial](https://www.questpdf.com/documentation/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://www.questpdf.com/documentation/api-reference.html)** - a detailed description of behavior of all available components and how to use them with C# Fluent API.
**[Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)** - everything that may help you designing great reports and reusable code that is easy to maintain.
## Example invoice