From 14f11232be05dd9754bf05e25f741c559eedb681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Wed, 9 Jun 2021 23:34:25 +0200 Subject: [PATCH] New elements: rotate, scale, translate --- QuestPDF.Examples/ElementExamples.cs | 756 +++++++++++++----- QuestPDF.Examples/Engine/ExampleTestBase.cs | 95 --- QuestPDF.Examples/Engine/FileNameAttribute.cs | 14 - QuestPDF.Examples/Engine/IgnoreAttribute.cs | 9 - .../Engine/ImageSizeAttribute.cs | 19 - QuestPDF.Examples/Engine/RenderingTest.cs | 56 ++ .../Engine/ShowResultAttribute.cs | 9 - QuestPDF.Examples/Engine/SimpleDocument.cs | 3 +- QuestPDF.Examples/FrameExample.cs | 32 +- QuestPDF.Examples/LoremPicsumExample.cs | 38 +- QuestPDF.Examples/Padding.cs | 241 +++--- QuestPDF/Elements/Rotate.cs | 55 ++ QuestPDF/Elements/Scale.cs | 60 ++ QuestPDF/Elements/Translate.cs | 23 + QuestPDF/Fluent/ElementExtensions.cs | 2 +- QuestPDF/Fluent/RotateExtensions.cs | 27 + QuestPDF/Fluent/ScaleExtensions.cs | 47 ++ QuestPDF/Fluent/TranslateExtensions.cs | 32 + 18 files changed, 1018 insertions(+), 500 deletions(-) delete mode 100644 QuestPDF.Examples/Engine/ExampleTestBase.cs delete mode 100644 QuestPDF.Examples/Engine/FileNameAttribute.cs delete mode 100644 QuestPDF.Examples/Engine/IgnoreAttribute.cs delete mode 100644 QuestPDF.Examples/Engine/ImageSizeAttribute.cs create mode 100644 QuestPDF.Examples/Engine/RenderingTest.cs delete mode 100644 QuestPDF.Examples/Engine/ShowResultAttribute.cs create mode 100644 QuestPDF/Elements/Rotate.cs create mode 100644 QuestPDF/Elements/Scale.cs create mode 100644 QuestPDF/Elements/Translate.cs create mode 100644 QuestPDF/Fluent/RotateExtensions.cs create mode 100644 QuestPDF/Fluent/ScaleExtensions.cs create mode 100644 QuestPDF/Fluent/TranslateExtensions.cs diff --git a/QuestPDF.Examples/ElementExamples.cs b/QuestPDF.Examples/ElementExamples.cs index 6384910..4388b04 100644 --- a/QuestPDF.Examples/ElementExamples.cs +++ b/QuestPDF.Examples/ElementExamples.cs @@ -1,4 +1,6 @@ +using System; using System.Linq; +using NUnit.Framework; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; @@ -7,215 +9,379 @@ using SkiaSharp; namespace QuestPDF.Examples { - public class ElementExamples : ExampleTestBase + [TestFixture] + public class ElementExamples { - //[ShowResult] - [ImageSize(200, 150)] - public void Placeholder(IContainer container) + [Test] + public void Placeholder() { - container - .Background("#FFF") - .Padding(25) - .Placeholder(); - } - - //[ShowResult] - [ImageSize(300, 300)] - public void Decoration(IContainer container) - { - container - .Background("#FFF") - .Padding(25) - .Decoration(decoration => + RenderingTest + .Create() + .PageSize(200, 150) + .Render(container => { - decoration - .Header() - .Background("#888") - .Padding(10) - .Text("Notes", TextStyle.Default.Size(16).Color("#FFF")); - - decoration - .Content() - .Background("#DDD") - .Padding(10) - .ExtendVertical() - .Text(Helpers.Placeholders.LoremIpsum()); + container + .Background("#FFF") + .Padding(25) + .Placeholder(); }); } - //[ShowResult] - [ImageSize(298, 421)] - public void Page(IContainer container) + [Test] + public void Decoration() { - container - .Background("#FFF") - .Padding(15) - .Page(page => + RenderingTest + .Create() + .PageSize(300, 300) + .Render(container => { - page.Header() - .Height(60) - .Background("#BBB") - .AlignCenter() - .AlignMiddle() - .Text("Header"); + container + .Background("#FFF") + .Padding(25) + .Decoration(decoration => + { + decoration + .Header() + .Background(Colors.Grey.Medium) + .Padding(10) + .Text("Notes", TextStyle.Default.Size(16).Color("#FFF")); - page.Content() - .Background("#DDD") - .AlignCenter() - .AlignMiddle() - .Text("Content"); + decoration + .Content() + .Background(Colors.Grey.Lighten3) + .Padding(10) + .ExtendVertical() + .Text(Helpers.Placeholders.LoremIpsum()); + }); + }); + } + + [Test] + public void Page() + { + RenderingTest + .Create() + .PageSize(298, 421) + .Render(container => + { + container + .Background("#FFF") + .Padding(15) + .Page(page => + { + page.Header() + .Height(60) + .Background(Colors.Grey.Lighten1) + .AlignCenter() + .AlignMiddle() + .Text("Header"); + + page.Content() + .Background(Colors.Grey.Lighten2) + .AlignCenter() + .AlignMiddle() + .Text("Content"); - page.Footer() - .Height(30) - .Background("#BBB") - .AlignCenter() - .AlignMiddle() - .Text("Footer"); + page.Footer() + .Height(30) + .Background(Colors.Grey.Lighten1) + .AlignCenter() + .AlignMiddle() + .Text("Footer"); + }); }); } - //[ShowResult] - [ImageSize(740, 200)] - public void Row(IContainer container) + [Test] + public void Row() { - container - .Background("#FFF") - .Padding(20) - .Stack(stack => + RenderingTest + .Create() + .PageSize(740, 200) + .Render(container => { - stack.Item() - .PaddingBottom(10) - .AlignCenter() - .Text("This Row element is 700pt wide"); + container + .Background("#FFF") + .Padding(20) + .Stack(stack => + { + stack.Item() + .PaddingBottom(10) + .AlignCenter() + .Text("This Row element is 700pt wide"); - stack.Item().Row(row => - { - row.ConstantColumn(100) - .Background("#DDD") - .Padding(10) - .ExtendVertical() - .Text("This column is 100 pt wide"); + stack.Item().Row(row => + { + row.ConstantColumn(100) + .Background(Colors.Grey.Lighten1) + .Padding(10) + .ExtendVertical() + .Text("This column is 100 pt wide"); - row.RelativeColumn() - .Background("#BBB") - .Padding(10) - .Text("This column takes 1/3 of the available space (200pt)"); + row.RelativeColumn() + .Background(Colors.Grey.Lighten2) + .Padding(10) + .Text("This column takes 1/3 of the available space (200pt)"); - row.RelativeColumn(2) - .Background("#DDD") - .Padding(10) - .Text("This column takes 2/3 of the available space (400pt)"); - }); + row.RelativeColumn(2) + .Background(Colors.Grey.Lighten3) + .Padding(10) + .Text("This column takes 2/3 of the available space (400pt)"); + }); + }); }); } - //[ShowResult] - [ImageSize(500, 350)] - public void Column(IContainer container) + [Test] + public void RowSpacing() { - container - .Background("#FFF") - .Padding(15) - .Stack(column => + RenderingTest + .Create() + .PageSize(740, 200) + .Render(container => { - column.Spacing(10); - - column - .Item() - .Background("#999") - .Height(50); - - column - .Item() - .Background("#BBB") - .Height(100); - - column - .Item() - .Background("#DDD") - .Height(150); + container + .Background("#FFF") + .Padding(20) + .Row(row => + { + row.Spacing(20); + row.RelativeColumn(2).Border(1).Background(Colors.Grey.Lighten1); + row.RelativeColumn(3).Border(1).Background(Colors.Grey.Lighten2); + row.RelativeColumn(4).Border(1).Background(Colors.Grey.Lighten3); + }); }); } - - [ShowResult] - [ImageSize(210, 210)] - public void Debug(IContainer container) + + [Test] + public void Stack() { - container - .Padding(25) - .Debug("Grid example", Colors.Blue.Medium) - .Grid(grid => + RenderingTest + .Create() + .PageSize(500, 360) + .Render(container => { - grid.Columns(3); - grid.Spacing(5); - - foreach (var _ in Enumerable.Range(0, 8)) - grid.Item().Height(50).Placeholder(); + container + .Background("#FFF") + .Padding(15) + .Stack(stack => + { + stack.Spacing(15); + + stack.Item().Background(Colors.Grey.Medium).Height(50); + stack.Item().Background(Colors.Grey.Lighten1).Height(100); + stack.Item().Background(Colors.Grey.Lighten2).Height(150); + }); }); } - //[ShowResult] - [ImageSize(300, 200)] - public void ElementEnd(IContainer container) + [Test] + public void Debug() { - var text = ""; + RenderingTest + .Create() + .PageSize(210, 210) + .Render(container => + { + container + .Padding(25) + .Debug("Grid example", Colors.Blue.Medium) + .Grid(grid => + { + grid.Columns(3); + grid.Spacing(5); + + foreach (var _ in Enumerable.Range(0, 8)) + grid.Item().Height(50).Placeholder(); + }); + }); + } + + [Test] + public void ElementEnd() + { + RenderingTest + .Create() + .PageSize(300, 200) + .Render(container => + { + var text = ""; - container - .Padding(10) - .Element(x => - { - if (string.IsNullOrWhiteSpace(text)) - x.Height(10).Width(50).Background("#DDD"); - else - x.Text(text); + container + .Padding(10) + .Element(x => + { + if (string.IsNullOrWhiteSpace(text)) + x.Height(10).Width(50).Background("#DDD"); + else + x.Text(text); + }); }); } - //[ShowResult] - [ImageSize(300, 200)] - public void GridExample(IContainer container) + [Test] + public void GridExample() { - var textStyle = TextStyle.Default.Size(14); + RenderingTest + .Create() + .PageSize(400, 230) + .Render(container => + { + var textStyle = TextStyle.Default.Size(14); - container - .Padding(20) - .AlignRight() - .Grid(grid => - { - grid.VerticalSpacing(20); - grid.HorizontalSpacing(10); - grid.Columns(12); + container + .Padding(15) + .AlignRight() + .Grid(grid => + { + grid.VerticalSpacing(15); + grid.HorizontalSpacing(15); + grid.AlignCenter(); + grid.Columns(10); // 12 by default - grid.Item(8).Background("#DDD").Height(50).Padding(5).Text("This is a short text", textStyle); - grid.Item(4).Background("#BBB").Padding(5).Text("Showing how to...", textStyle); - grid.Item(2).Background("#999").Height(50); - grid.Item(4).Background("#AAA").Border(2).BorderColor("#666").Padding(5).Text("...generate", textStyle); - grid.Item(6).Background("#CCC").Padding(5).Text("simple grids", textStyle.Size(18).Bold()); - grid.Item(8).Background("#DDD").Height(50); - }); - } - - //[ShowResult] - [ImageSize(300, 300)] - public void RandomColorMatrix(IContainer container) - { - container - .Padding(25) - .Grid(grid => - { - grid.Columns(5); + grid.Item(6).Background(Colors.Blue.Lighten1).Height(50); + grid.Item(4).Background(Colors.Blue.Lighten3).Height(50); - Enumerable - .Range(0, 25) - .Select(x => Placeholders.BackgroundColor()) - .ToList() - .ForEach(x => grid.Item().Height(50).Background(x)); + grid.Item(2).Background(Colors.Teal.Lighten1).Height(70); + grid.Item(3).Background(Colors.Teal.Lighten2).Height(70); + grid.Item(5).Background(Colors.Teal.Lighten3).Height(70); + + grid.Item(2).Background(Colors.Green.Lighten1).Height(50); + grid.Item(2).Background(Colors.Green.Lighten2).Height(50); + grid.Item(2).Background(Colors.Green.Lighten3).Height(50); + }); }); } - //[ShowResult] - [ImageSize(450, 150)] - public void DefinedColors(IContainer container) + [Test] + public void Canvas() + { + RenderingTest + .Create() + .PageSize(300, 200) + .Render(container => + { + container + .Background("#FFF") + .Padding(25) + .Canvas((canvas, size) => + { + using var paint = new SKPaint + { + Color = SKColors.Red, + StrokeWidth = 10, + IsStroke = true + }; + + // move origin to the center of the available space + canvas.Translate(size.Width / 2, size.Height / 2); + + // draw a circle + canvas.DrawCircle(0, 0, 50, paint); + }); + }); + } + + [Test] + public void LayersExample() + { + RenderingTest + .Create() + .PageSize(400, 250) + .Render(container => + { + container + .Padding(25) + .Layers(layers => + { + // layer below main content + layers + .Layer() + .Height(100) + .Width(100) + .Background(Colors.Grey.Lighten3); + + layers + .PrimaryLayer() + .Padding(25) + .Stack(stack => + { + stack.Spacing(5); + + foreach (var _ in Enumerable.Range(0, 7)) + stack.Item().Text(Placeholders.Sentence()); + }); + + // layer above the main content + layers + .Layer() + .AlignCenter() + .AlignMiddle() + .Text("Watermark", TextStyle.Default.Size(48).Bold().Color(Colors.Green.Lighten3)); + + layers + .Layer() + .AlignBottom() + .PageNumber("Page {number}", TextStyle.Default.Size(16).Color(Colors.Green.Medium)); + }); + }); + } + + [Test] + public void EnsureSpace() + { + RenderingTest + .Create() + .PageSize(300, 400) + .Render(container => + { + container + .Padding(50) + .Page(page => + { + page.Header().PageNumber("Page {number}"); + + page.Content().Height(300).Stack(content => + { + content.Item().Height(200).Background(Colors.Grey.Lighten2); + + content.Item().EnsureSpace(100).Stack(stack => + { + stack.Spacing(10); + + foreach (var _ in Enumerable.Range(0, 4)) + stack.Item().Height(50).Background(Colors.Green.Lighten1); + }); + }); + }); + }); + } + + [Test] + public void RandomColorMatrix() + { + RenderingTest + .Create() + .PageSize(300, 300) + .Render(container => + { + container + .Padding(25) + .Grid(grid => + { + grid.Columns(5); + + Enumerable + .Range(0, 25) + .Select(x => Placeholders.BackgroundColor()) + .ToList() + .ForEach(x => grid.Item().Height(50).Background(x)); + }); + }); + } + + [Test] + public void DefinedColors() { var colors = new[] { @@ -238,19 +404,24 @@ namespace QuestPDF.Examples Colors.Green.Accent4, }; - container - .Padding(25) - .Height(100) - .Row(row => + RenderingTest + .Create() + .PageSize(450, 150) + .Render(container => { - foreach (var color in colors) - row.RelativeColumn().Background(color); + container + .Padding(25) + .Height(100) + .Row(row => + { + foreach (var color in colors) + row.RelativeColumn().Background(color); + }); }); } - - //[ShowResult] - [ImageSize(500, 175)] - public void DefinedFonts(IContainer container) + + [Test] + public void DefinedFonts() { var fonts = new[] { @@ -265,54 +436,205 @@ namespace QuestPDF.Examples Fonts.ComicSans }; - container - .Padding(25) - .Grid(grid => + RenderingTest + .Create() + .PageSize(500, 175) + .Render(container => { - grid.Columns(3); + container + .Padding(25) + .Grid(grid => + { + grid.Columns(3); - foreach (var font in fonts) - { - grid.Item() - .Border(1) - .BorderColor(Colors.Grey.Medium) - .Padding(10) - .Text(font, TextStyle.Default.FontType(font).Size(16)); - } + foreach (var font in fonts) + { + grid.Item() + .Border(1) + .BorderColor(Colors.Grey.Medium) + .Padding(10) + .Text(font, TextStyle.Default.FontType(font).Size(16)); + } + }); + }); + } + + [Test] + public void Layers() + { + RenderingTest + .Create() + .PageSize(300, 300) + .Render(container => + { + container + .Background("#FFF") + .Padding(25) + .Layers(layers => + { + layers.Layer().Text("Something else"); + + layers.PrimaryLayer().Stack(stack => + { + stack.Item().PaddingTop(20).Text("Text 1"); + stack.Item().PaddingTop(40).Text("Text 2"); + }); + + layers.Layer().Canvas((canvas, size) => + { + using var paint = new SKPaint + { + Color = SKColors.Red, + StrokeWidth = 5 + }; + + canvas.Translate(size.Width / 2, size.Height / 2); + canvas.DrawCircle(0, 0, 50, paint); + }); + + layers.Layer().Background("#8F00").Extend(); + layers.Layer().PaddingTop(40).Text("It works!", TextStyle.Default.Size(24)); + }); + }); + } + + [Test] + public void Box() + { + RenderingTest + .Create() + .PageSize(300, 150) + .Render(container => + { + container + .Background("#FFF") + .Padding(15) + .Border(4) + .BorderColor(Colors.Blue.Medium) + //.Box() + .Background(Colors.Grey.Lighten2) + .Padding(15) + .Text("Test of the \n box element", TextStyle.Default.Size(20)); + }); + } + + [Test] + public void Scale() + { + RenderingTest + .Create() + .PageSize(400, 400) + .Render(container => + { + var style = TextStyle.Default.Size(20); + + container + .Background("#FFF") + .Padding(25) + .Stack(stack => + { + stack.Spacing(5); + stack.Item().Background(Placeholders.BackgroundColor()).Scale(0.5f).Text("Smaller text", style); + stack.Item().Background(Placeholders.BackgroundColor()).Text("Normal text", style); + stack.Item().Background(Placeholders.BackgroundColor()).Scale(2f).Text("Bigger text", style); + stack.Item().Background(Placeholders.BackgroundColor()).Scale(-1.5f).Text("Flipped text", style); + }); + }); + } + + [Test] + public void Translate() + { + RenderingTest + .Create() + .PageSize(500, 300) + .Render(container => + { + container + .Background("#FFF") + .Padding(25) + .Box() + .Background(Colors.Green.Lighten4) + .Padding(5) + .TranslateX(15) + .TranslateY(15) + .Text("Text outside of bounds"); + }); + } + + [Test] + public void Rotate() + { + RenderingTest + .Create() + .PageSize(450, 450) + .Render(container => + { + container + .Background("#FFF") + .Padding(20) + .Grid(grid => + { + grid.Columns(2); + grid.Spacing(10); + + foreach (var turns in Enumerable.Range(0, 4)) + { + grid.Item() + .Width(200) + .Height(200) + .Background(Colors.Grey.Lighten3) + .Padding(10) + //.Box() + .Element(element => + { + foreach (var x in Enumerable.Range(0, turns)) + element = element.RotateRight(); + + return element; + }) + .Text($"Rotated {turns * 90} degrees.", TextStyle.Default.Size(14)); + } + }); }); } - //[ShowResult] - [ImageSize(300, 300)] - public void Layers(IContainer container) + [Test] + public void Flip() { - container - .Background("#FFF") - .Padding(25) - .Layers(layers => + RenderingTest + .Create() + .PageSize(450, 450) + .Render(container => { - layers.Layer().Text("Something else"); - - layers.PrimaryLayer().Stack(stack => - { - stack.Item().PaddingTop(20).Text("Text 1"); - stack.Item().PaddingTop(40).Text("Text 2"); - }); - - layers.Layer().Canvas((canvas, size) => - { - using var paint = new SKPaint + container + .Background("#FFF") + .Padding(20) + .Grid(grid => { - Color = SKColors.Red, - StrokeWidth = 5 - }; - - canvas.Translate(size.Width / 2, size.Height / 2); - canvas.DrawCircle(0, 0, 50, paint); - }); - - layers.Layer().Background("#8F00").Extend(); - layers.Layer().PaddingTop(40).Text("It works!", TextStyle.Default.Size(24)); + grid.Columns(2); + grid.Spacing(10); + + foreach (var turns in Enumerable.Range(0, 4)) + { + grid.Item() + .Width(200) + .Height(200) + .Background(Colors.Grey.Lighten3) + .Padding(10) + .Element(element => + { + if (turns == 1 || turns == 2) + element = element.FlipX(); + + if (turns == 2 || turns == 3) + element = element.FlipY(); + + return element; + }) + .Text($"Flipped.", TextStyle.Default.Size(14)); + } + }); }); } } diff --git a/QuestPDF.Examples/Engine/ExampleTestBase.cs b/QuestPDF.Examples/Engine/ExampleTestBase.cs deleted file mode 100644 index c864af1..0000000 --- a/QuestPDF.Examples/Engine/ExampleTestBase.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using NUnit.Framework; -using QuestPDF.Elements; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; -using SkiaSharp; - -namespace QuestPDF.Examples.Engine -{ - [TestFixture] - public class ExampleTestBase - { - private readonly Size DefaultImageSize = new Size(400, 300); - private const string ResultPath = "Result"; - - [SetUp] - public void Setup() - { - if (Directory.Exists(ResultPath)) - Directory.Delete(ResultPath, true); - - Directory.CreateDirectory(ResultPath); - } - - [Test] - public void RunTest() - { - GetType() - .GetMethods() - .Where(IsExampleMethod) - .ToList() - .ForEach(HandleExample); - } - - private bool IsExampleMethod(MethodInfo method) - { - var parameters = method.GetParameters(); - return parameters.Length == 1 && parameters.First().ParameterType == typeof(IContainer); - } - - private T GetAttribute(MethodInfo methodInfo) where T : Attribute - { - return methodInfo.GetCustomAttributes().FirstOrDefault(y => y is T) as T; - } - - private void HandleExample(MethodInfo methodInfo) - { - var size = GetAttribute(methodInfo)?.Size ?? DefaultImageSize; - var fileName = GetAttribute(methodInfo)?.FileName ?? methodInfo.Name; - var showResult = GetAttribute(methodInfo) != null; - var shouldIgnore = GetAttribute(methodInfo) != null; - - if (shouldIgnore) - return; - - var container = new Container(); - methodInfo.Invoke(this, new object[] {container}); - - Func fileNameSchema = i => $"{fileName.ToLower()}-${i}.png"; - - try - { - var document = new SimpleDocument(container, size); - document.GenerateImages(fileNameSchema); - } - catch (Exception e) - { - throw new Exception($"Cannot perform test {fileName}", e); - } - - if (showResult) - Process.Start("explorer", fileNameSchema(0)); - } - - private byte[] RenderPage(Element element, Size size) - { - // scale the result so it is more readable - const float scalingFactor = 2; - - var imageInfo = new SKImageInfo((int)(size.Width * scalingFactor), (int)(size.Height * scalingFactor)); - using var surface = SKSurface.Create(imageInfo); - surface.Canvas.Scale(scalingFactor); - - var canvas = new Drawing.Canvas(surface.Canvas); - element?.Draw(canvas, size); - - surface.Canvas.Save(); - return surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray(); - } - } -} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/FileNameAttribute.cs b/QuestPDF.Examples/Engine/FileNameAttribute.cs deleted file mode 100644 index a569ddf..0000000 --- a/QuestPDF.Examples/Engine/FileNameAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace QuestPDF.Examples.Engine -{ - public class FileNameAttribute : Attribute - { - public string FileName { get; } - - public FileNameAttribute(string fileName) - { - FileName = fileName; - } - } -} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/IgnoreAttribute.cs b/QuestPDF.Examples/Engine/IgnoreAttribute.cs deleted file mode 100644 index ba1fc47..0000000 --- a/QuestPDF.Examples/Engine/IgnoreAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace QuestPDF.Examples.Engine -{ - public class IgnoreAttribute : Attribute - { - - } -} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/ImageSizeAttribute.cs b/QuestPDF.Examples/Engine/ImageSizeAttribute.cs deleted file mode 100644 index d0b17ae..0000000 --- a/QuestPDF.Examples/Engine/ImageSizeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using QuestPDF.Infrastructure; - -namespace QuestPDF.Examples.Engine -{ - public class ImageSizeAttribute : Attribute - { - private int Width { get; } - private int Height { get; } - - public Size Size => new Size(Width, Height); - - public ImageSizeAttribute(int width, int height) - { - Width = width; - Height = height; - } - } -} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/RenderingTest.cs b/QuestPDF.Examples/Engine/RenderingTest.cs new file mode 100644 index 0000000..f8ef13d --- /dev/null +++ b/QuestPDF.Examples/Engine/RenderingTest.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using QuestPDF.Elements; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples.Engine +{ + public class RenderingTest + { + private string FileNamePrefix = "test"; + private decimal ImageDpi = 2; + private Size Size { get; set; } + + private RenderingTest() + { + + } + + public static RenderingTest Create() + { + return new RenderingTest(); + } + + public RenderingTest FileName(string fileName) + { + FileNamePrefix = fileName; + return this; + } + + public RenderingTest Dpi(decimal value = 2) + { + ImageDpi = value; + return this; + } + + public RenderingTest PageSize(int width, int height) + { + Size = new Size(width, height); + return this; + } + + public void Render(Action content) + { + var container = new Container(); + content.Invoke(container); + + Func fileNameSchema = i => $"{FileNamePrefix}-${i}.png"; + + var document = new SimpleDocument(container, Size); + document.GenerateImages(fileNameSchema); + + Process.Start("explorer", fileNameSchema(0)); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/ShowResultAttribute.cs b/QuestPDF.Examples/Engine/ShowResultAttribute.cs deleted file mode 100644 index 44ff885..0000000 --- a/QuestPDF.Examples/Engine/ShowResultAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace QuestPDF.Examples.Engine -{ - public class ShowResultAttribute : Attribute - { - - } -} \ No newline at end of file diff --git a/QuestPDF.Examples/Engine/SimpleDocument.cs b/QuestPDF.Examples/Engine/SimpleDocument.cs index c8fd33c..1be8664 100644 --- a/QuestPDF.Examples/Engine/SimpleDocument.cs +++ b/QuestPDF.Examples/Engine/SimpleDocument.cs @@ -21,7 +21,8 @@ namespace QuestPDF.Examples.Engine return new DocumentMetadata() { RasterDpi = PageSizes.PointsPerInch * 2, - Size = Size + Size = Size, + DocumentLayoutExceptionThreshold = 10 }; } diff --git a/QuestPDF.Examples/FrameExample.cs b/QuestPDF.Examples/FrameExample.cs index 52d82fd..319f1fb 100644 --- a/QuestPDF.Examples/FrameExample.cs +++ b/QuestPDF.Examples/FrameExample.cs @@ -1,3 +1,4 @@ +using NUnit.Framework; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; @@ -19,25 +20,30 @@ namespace QuestPDF.Examples public static IContainer ValueCell(this IContainer container) => container.Cell(false); } - public class FrameExample: ExampleTestBase + public class FrameExample { - [ImageSize(550, 400)] - [ShowResult] + [Test] public void Frame(IContainer container) { - container - .Background("#FFF") - .Padding(25) - .Stack(stack => + RenderingTest + .Create() + .PageSize(550, 400) + .Render(container => { - for(var i=1; i<=4; i++) - { - stack.Item().Row(row => + container + .Background("#FFF") + .Padding(25) + .Stack(stack => { - row.RelativeColumn(2).LabelCell().Text(Placeholders.Label()); - row.RelativeColumn(3).ValueCell().Text(Placeholders.Paragraph()); + for(var i=1; i<=4; i++) + { + stack.Item().Row(row => + { + row.RelativeColumn(2).LabelCell().Text(Placeholders.Label()); + row.RelativeColumn(3).ValueCell().Text(Placeholders.Paragraph()); + }); + } }); - } }); } } diff --git a/QuestPDF.Examples/LoremPicsumExample.cs b/QuestPDF.Examples/LoremPicsumExample.cs index 184090e..c20094a 100644 --- a/QuestPDF.Examples/LoremPicsumExample.cs +++ b/QuestPDF.Examples/LoremPicsumExample.cs @@ -1,4 +1,5 @@ using System.Net; +using NUnit.Framework; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Infrastructure; @@ -27,27 +28,32 @@ namespace QuestPDF.Examples } } - public class LoremPicsumExample : ExampleTestBase + public class LoremPicsumExample { - [ShowResult] - [ImageSize(350, 280)] - public void LoremPicsum(IContainer container) + [Test] + public void LoremPicsum() { - container - .Background("#FFF") - .Padding(25) - .Stack(column => + RenderingTest + .Create() + .PageSize(350, 280) + .Render(container => { - column.Spacing(10); + container + .Background("#FFF") + .Padding(25) + .Stack(column => + { + column.Spacing(10); - column - .Item() - .Component(new LoremPicsum(true)); + column + .Item() + .Component(new LoremPicsum(true)); - column - .Item() - .AlignRight() - .Text("From Lorem Picsum"); + column + .Item() + .AlignRight() + .Text("From Lorem Picsum"); + }); }); } } diff --git a/QuestPDF.Examples/Padding.cs b/QuestPDF.Examples/Padding.cs index 0a7424a..6a3446f 100644 --- a/QuestPDF.Examples/Padding.cs +++ b/QuestPDF.Examples/Padding.cs @@ -1,132 +1,161 @@ -using QuestPDF.Examples.Engine; +using NUnit.Framework; +using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.Examples { - public class Examples : ExampleTestBase + public class Examples { - public void Padding(IContainer container) + [Test] + public void Padding() { - container - .Background("#FDD") - .Padding(50) - - .Background("#AFA") - .PaddingVertical(50) - - .Background("#77F") - .PaddingHorizontal(50) - - .Background("#444"); - } - - public void Border(IContainer container) - { - container - .Background("#EEE") - .Padding(25) - - .AlignBottom() - .AlignCenter() - .BorderBottom(2) - .BorderColor("#000") - - .Background("FFF") - .Padding(5) - .Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji").Alignment(HorizontalAlignment.Center)); - } - - public void Alignment(IContainer container) - { - container - .Stack(column => + RenderingTest + .Create() + .PageSize(200, 150) + .Render(container => { - column - .Item() - .Height(100) - .Background("#FFF") - - .AlignLeft() - .AlignMiddle() + container + .Background("#FDD") + .Padding(50) + + .Background("#AFA") + .PaddingVertical(50) + + .Background("#77F") + .PaddingHorizontal(50) - .Width(50) - .Height(50) .Background("#444"); - - column - .Item() - .Height(100) - .Background("#DDD") - - .AlignCenter() - .AlignMiddle() - - .Width(50) - .Height(50) - .Background("#222"); - - column - .Item() - .Height(100) - .Background("#BBB") - - .AlignRight() - .AlignMiddle() - - .Width(50) - .Height(50) - .Background("#000"); }); } - public void Expand(IContainer container) + [Test] + public void Border() { - container - .Stack(column => + RenderingTest + .Create() + .PageSize(200, 150) + .Render(container => { - column - .Item() - .Height(150) - .Row(row => + container + .Background("#EEE") + .Padding(25) + + .AlignBottom() + .AlignCenter() + .BorderBottom(2) + .BorderColor("#000") + + .Background("FFF") + .Padding(5) + .Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji").Alignment(HorizontalAlignment.Center)); + }); + } + + [Test] + public void Alignment() + { + RenderingTest + .Create() + .PageSize(200, 150) + .Render(container => + { + container + .Stack(column => { - row.RelativeColumn() - .Extend() - .Background("FFF") + column + .Item() + .Height(100) + .Background("#FFF") + + .AlignLeft() + .AlignMiddle() - .Height(50) .Width(50) - .Background("444"); - - row.RelativeColumn() - .Extend() - .Background("BBB") - .Height(50) - .ExtendHorizontal() - .Background("444"); - }); + .Background("#444"); - column - .Item() - .Height(150) - .Row(row => - { - row.RelativeColumn() - .Extend() - .Background("BBB") + column + .Item() + .Height(100) + .Background("#DDD") + + .AlignCenter() + .AlignMiddle() - .ExtendVertical() .Width(50) - .Background("444"); - - row.RelativeColumn() - .Extend() - .Background("BBB") + .Height(50) + .Background("#222"); + + column + .Item() + .Height(100) + .Background("#BBB") + + .AlignRight() + .AlignMiddle() - .ExtendVertical() - .ExtendHorizontal() - .Background("444"); + .Width(50) + .Height(50) + .Background("#000"); + }); + }); + } + + [Test] + public void Expand() + { + RenderingTest + .Create() + .PageSize(200, 150) + .Render(container => + { + container + .Stack(column => + { + column + .Item() + .Height(150) + .Row(row => + { + row.RelativeColumn() + .Extend() + .Background("FFF") + + .Height(50) + .Width(50) + .Background("444"); + + row.RelativeColumn() + .Extend() + .Background("BBB") + + .Height(50) + .ExtendHorizontal() + .Background("444"); + }); + + column + .Item() + .Height(150) + .Row(row => + { + row.RelativeColumn() + .Extend() + .Background("BBB") + + .ExtendVertical() + .Width(50) + .Background("444"); + + row.RelativeColumn() + .Extend() + .Background("BBB") + + .ExtendVertical() + .ExtendHorizontal() + .Background("444"); + }); }); }); } diff --git a/QuestPDF/Elements/Rotate.cs b/QuestPDF/Elements/Rotate.cs new file mode 100644 index 0000000..6555299 --- /dev/null +++ b/QuestPDF/Elements/Rotate.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class Rotate : ContainerElement + { + public int TurnCount { get; set; } + public int NormalizedTurnCount => (TurnCount % 4 + 4) % 4; + + internal override ISpacePlan Measure(Size availableSpace) + { + if (NormalizedTurnCount == 0 || NormalizedTurnCount == 2) + return base.Measure(availableSpace); + + availableSpace = new Size(availableSpace.Height, availableSpace.Width); + var childSpace = base.Measure(availableSpace) as Size; + + if (childSpace == null) + return new Wrap(); + + var targetSpace = new Size(childSpace.Height, childSpace.Width); + + if (childSpace is FullRender) + return new FullRender(targetSpace); + + if (childSpace is PartialRender) + return new PartialRender(targetSpace); + + throw new ArgumentException(); + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var skiaCanvas = (canvas as Drawing.Canvas)?.SkiaCanvas; + + if (skiaCanvas == null) + return; + + var currentMatrix = skiaCanvas.TotalMatrix; + + if (NormalizedTurnCount % 4 == 1 || NormalizedTurnCount % 4 == 2) + skiaCanvas.Translate(availableSpace.Width, 0); + + if (NormalizedTurnCount % 4 == 2 || NormalizedTurnCount % 4 == 3) + skiaCanvas.Translate(0, availableSpace.Height); + + skiaCanvas.RotateRadians(TurnCount * (float) Math.PI / 2f); + Child?.Draw(canvas, availableSpace); + skiaCanvas.SetMatrix(currentMatrix); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Scale.cs b/QuestPDF/Elements/Scale.cs new file mode 100644 index 0000000..ca5a3f7 --- /dev/null +++ b/QuestPDF/Elements/Scale.cs @@ -0,0 +1,60 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class Scale : ContainerElement + { + public float ScaleX { get; set; } = 1; + public float ScaleY { get; set; } = 1; + + internal override ISpacePlan Measure(Size availableSpace) + { + var targetSpace = new Size( + Math.Abs(availableSpace.Width / ScaleX), + Math.Abs(availableSpace.Height / ScaleY)); + + var measure = base.Measure(targetSpace) as Size; + + if (measure == null) + return new Wrap(); + + var targetSize = new Size( + Math.Abs(measure.Width * ScaleX), + Math.Abs(measure.Height * ScaleY)); + + if (measure is PartialRender) + return new PartialRender(targetSize); + + if (measure is FullRender) + return new FullRender(targetSize); + + throw new ArgumentException(); + } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var skiaCanvas = (canvas as Drawing.Canvas)?.SkiaCanvas; + + if (skiaCanvas == null) + return; + + var targetSpace = new Size( + Math.Abs(availableSpace.Width / ScaleX), + Math.Abs(availableSpace.Height / ScaleY)); + + var currentMatrix = skiaCanvas.TotalMatrix; + + if (ScaleX < 0) + skiaCanvas.Translate(availableSpace.Width, 0); + + if (ScaleY < 0) + skiaCanvas.Translate(0, availableSpace.Height); + + skiaCanvas.Scale(ScaleX, ScaleY); + Child?.Draw(canvas, targetSpace); + skiaCanvas.SetMatrix(currentMatrix); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Translate.cs b/QuestPDF/Elements/Translate.cs new file mode 100644 index 0000000..51e21a5 --- /dev/null +++ b/QuestPDF/Elements/Translate.cs @@ -0,0 +1,23 @@ + +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class Translate : ContainerElement + { + public float TranslateX { get; set; } = 1; + public float TranslateY { get; set; } = 1; + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var skiaCanvas = (canvas as Drawing.Canvas)?.SkiaCanvas; + + if (skiaCanvas == null) + return; + + skiaCanvas.Translate(TranslateX, TranslateY); + base.Draw(canvas, availableSpace); + skiaCanvas.Translate(-TranslateX, -TranslateY); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index e1d8e9f..d6f7261 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -152,7 +152,7 @@ namespace QuestPDF.Fluent public static void Canvas(this IContainer element, DrawOnCanvas handler) { - element.Element(new Elements.Canvas + element.Element(new Canvas { Handler = handler }); diff --git a/QuestPDF/Fluent/RotateExtensions.cs b/QuestPDF/Fluent/RotateExtensions.cs new file mode 100644 index 0000000..eff3b2d --- /dev/null +++ b/QuestPDF/Fluent/RotateExtensions.cs @@ -0,0 +1,27 @@ +using System; +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class RotateExtensions + { + private static IContainer Rotate(this IContainer element, Action handler) + { + var scale = element as Rotate ?? new Rotate(); + handler(scale); + + return element.Element(scale); + } + + public static IContainer RotateLeft(this IContainer element) + { + return element.Rotate(x => x.TurnCount--); + } + + public static IContainer RotateRight(this IContainer element) + { + return element.Rotate(x => x.TurnCount++); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/ScaleExtensions.cs b/QuestPDF/Fluent/ScaleExtensions.cs new file mode 100644 index 0000000..6e1e9ca --- /dev/null +++ b/QuestPDF/Fluent/ScaleExtensions.cs @@ -0,0 +1,47 @@ +using System; +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class ScaleExtensions + { + private static IContainer Scale(this IContainer element, Action handler) + { + var scale = element as Scale ?? new Scale(); + handler(scale); + + return element.Element(scale); + } + + public static IContainer Scale(this IContainer element, float value) + { + return element.ScaleX(value).ScaleY(value); + } + + public static IContainer ScaleX(this IContainer element, float value) + { + return element.Scale(x => x.ScaleX = value); + } + + public static IContainer FlipX(this IContainer element) + { + return element.ScaleX(-1); + } + + public static IContainer ScaleY(this IContainer element, float value) + { + return element.Scale(x => x.ScaleY = value); + } + + public static IContainer FlipY(this IContainer element) + { + return element.ScaleY(-1); + } + + public static IContainer FlipOver(this IContainer element) + { + return element.FlipX().FlipY(); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/TranslateExtensions.cs b/QuestPDF/Fluent/TranslateExtensions.cs new file mode 100644 index 0000000..975a478 --- /dev/null +++ b/QuestPDF/Fluent/TranslateExtensions.cs @@ -0,0 +1,32 @@ +using System; +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class TranslateExtensions + { + private static IContainer Translate(this IContainer element, Action handler) + { + var translate = element as Translate ?? new Translate(); + handler(translate); + + return element.Element(translate); + } + + public static IContainer Translate(this IContainer element, float value) + { + return element.TranslateX(value).TranslateY(value); + } + + public static IContainer TranslateX(this IContainer element, float value) + { + return element.Translate(x => x.TranslateX = value); + } + + public static IContainer TranslateY(this IContainer element, float value) + { + return element.Translate(x => x.TranslateY = value); + } + } +} \ No newline at end of file