From 35214892ef162ef96ee04b98293a468d4049a966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Mon, 8 Feb 2021 14:36:12 +0100 Subject: [PATCH] 2021.2.0 Internal links, external links, dynamic images, font weights --- .../Layouts/SectionTemplate.cs | 4 +- .../Layouts/StandardReport.cs | 9 +- .../Layouts/TableOfContentsTemplate.cs | 51 ++++ QuestPDF.ReportSample/Typography.cs | 2 +- QuestPDF.UnitTests/AlignmentTests.cs | 2 +- QuestPDF.UnitTests/AspectRatioTests.cs | 2 +- QuestPDF.UnitTests/BackgroundTests.cs | 2 +- QuestPDF.UnitTests/BorderTests.cs | 2 +- QuestPDF.UnitTests/ConstrainedTests.cs | 2 +- QuestPDF.UnitTests/DebugTests.cs | 2 +- QuestPDF.UnitTests/ExtendTests.cs | 2 +- QuestPDF.UnitTests/Helpers.cs | 4 +- QuestPDF.UnitTests/PaddingTests.cs | 2 +- QuestPDF.UnitTests/ShowOnceTest.cs | 2 +- QuestPDF.UnitTests/StackTests.cs | 5 +- QuestPDF.UnitTests/TestEngine/CanvasMock.cs | 43 ++++ QuestPDF.UnitTests/TestEngine/ElementMock.cs | 16 ++ .../TestEngine/OperationBase.cs | 7 + .../Operations/CanvasDrawImageOperation.cs | 16 ++ .../CanvasDrawRectangleOperation.cs | 18 ++ .../Operations/CanvasDrawTextOperation.cs | 18 ++ .../Operations/CanvasTranslateOperation.cs | 14 ++ .../Operations/ChildDrawOperation.cs | 16 ++ .../Operations/ChildMeasureOperation.cs | 19 ++ .../Operations/ElementMeasureOperation.cs | 12 + .../TestEngine/SingleChildTests.cs | 31 +++ .../TestPlan.cs} | 228 ++---------------- QuestPDF/Drawing/Canvas.cs | 12 +- QuestPDF/Drawing/CanvasCache.cs | 8 +- QuestPDF/Drawing/DocumentGenerator.cs | 1 - QuestPDF/Elements/ExternalLink.cs | 21 ++ QuestPDF/Elements/InternalLink.cs | 21 ++ QuestPDF/Elements/InternalLocation.cs | 17 ++ QuestPDF/Elements/Stack.cs | 17 +- QuestPDF/Elements/Watermark.cs | 62 ----- QuestPDF/Fluent/ElementExtensions.cs | 30 ++- QuestPDF/Fluent/TextStyleExtensions.cs | 74 +++++- QuestPDF/Infrastructure/FontWeight.cs | 16 ++ QuestPDF/Infrastructure/ICanvas.cs | 4 +- QuestPDF/Infrastructure/TextStyle.cs | 16 +- QuestPDF/QuestPDF.csproj | 4 +- 41 files changed, 512 insertions(+), 322 deletions(-) create mode 100644 QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs create mode 100644 QuestPDF.UnitTests/TestEngine/CanvasMock.cs create mode 100644 QuestPDF.UnitTests/TestEngine/ElementMock.cs create mode 100644 QuestPDF.UnitTests/TestEngine/OperationBase.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs create mode 100644 QuestPDF.UnitTests/TestEngine/SingleChildTests.cs rename QuestPDF.UnitTests/{MeasureTest/MeasureTestMock.cs => TestEngine/TestPlan.cs} (50%) create mode 100644 QuestPDF/Elements/ExternalLink.cs create mode 100644 QuestPDF/Elements/InternalLink.cs create mode 100644 QuestPDF/Elements/InternalLocation.cs delete mode 100644 QuestPDF/Elements/Watermark.cs create mode 100644 QuestPDF/Infrastructure/FontWeight.cs diff --git a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs index c37b070..077c533 100644 --- a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs @@ -25,11 +25,11 @@ namespace QuestPDF.ReportSample.Layouts .PaddingBottom(5) .Text(Model.Title, Typography.Headline); - section.Content().PageableStack(column => + section.Content().PageableStack(stack => { foreach (var part in Model.Parts) { - column.Element().Row(row => + stack.Element().Row(row => { row.ConstantColumn(150).DarkCell().Text(part.Label, Typography.Normal); var frame = row.RelativeColumn().LightCell(); diff --git a/QuestPDF.ReportSample/Layouts/StandardReport.cs b/QuestPDF.ReportSample/Layouts/StandardReport.cs index faa39bc..99e1b9f 100644 --- a/QuestPDF.ReportSample/Layouts/StandardReport.cs +++ b/QuestPDF.ReportSample/Layouts/StandardReport.cs @@ -71,7 +71,7 @@ namespace QuestPDF.ReportSample.Layouts }); - row.ConstantColumn(150).Image(Model.LogoData); + row.ConstantColumn(150).ExternalLink("https://www.questpdf.com").Image(Model.LogoData); }); } @@ -81,11 +81,14 @@ namespace QuestPDF.ReportSample.Layouts { stack.Spacing(20); + stack.Element().Component(new TableOfContentsTemplate(Model.Sections)); + foreach (var section in Model.Sections) - stack.Element().Component(new SectionTemplate(section)); + stack.Element().Location(section.Title).Component(new SectionTemplate(section)); stack.Element().PageBreak(); - + stack.Element().Location("Photos"); + foreach (var photo in Model.Photos) stack.Element().Component(new PhotoTemplate(photo)); }); diff --git a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs new file mode 100644 index 0000000..31640a2 --- /dev/null +++ b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; + +namespace QuestPDF.ReportSample.Layouts +{ + public class TableOfContentsTemplate : IComponent + { + private List Sections { get; } + + public TableOfContentsTemplate(List sections) + { + Sections = sections; + } + + public void Compose(IContainer container) + { + container + .Section(section => + { + section + .Header() + .PaddingBottom(5) + .Text("Table of contents", Typography.Headline); + + section.Content().PageableStack(stack => + { + stack.Spacing(5); + + for (var i = 0; i < Sections.Count; i++) + stack.Element(c => DrawLink(c, i+1, Sections[i].Title)); + + stack.Element(c => DrawLink(c, Sections.Count+1, "Photos")); + }); + }); + } + + private void DrawLink(IContainer container, int number, string locationName) + { + container + .InternalLink(locationName) + .Row(row => + { + row.ConstantColumn(25).Text($"{number}.", Typography.Normal); + row.RelativeColumn().Text(locationName, Typography.Normal); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.ReportSample/Typography.cs b/QuestPDF.ReportSample/Typography.cs index b454d1b..72f6ab2 100644 --- a/QuestPDF.ReportSample/Typography.cs +++ b/QuestPDF.ReportSample/Typography.cs @@ -5,7 +5,7 @@ namespace QuestPDF.ReportSample { public static class Typography { - public static TextStyle Title => TextStyle.Default.FontType("Helvetica").Color("#000000").Size(20); + public static TextStyle Title => TextStyle.Default.FontType("Helvetica").Color("#000000").Size(20).Bold(); public static TextStyle Headline => TextStyle.Default.FontType("Helvetica").Color("#047AED").Size(14); public static TextStyle Normal => TextStyle.Default.FontType("Helvetica").Color("#000000").Size(10).LineHeight(1.25f).AlignLeft(); } diff --git a/QuestPDF.UnitTests/AlignmentTests.cs b/QuestPDF.UnitTests/AlignmentTests.cs index 185dde1..004a32e 100644 --- a/QuestPDF.UnitTests/AlignmentTests.cs +++ b/QuestPDF.UnitTests/AlignmentTests.cs @@ -2,7 +2,7 @@ using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/AspectRatioTests.cs b/QuestPDF.UnitTests/AspectRatioTests.cs index d1ffd84..7feb54e 100644 --- a/QuestPDF.UnitTests/AspectRatioTests.cs +++ b/QuestPDF.UnitTests/AspectRatioTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/BackgroundTests.cs b/QuestPDF.UnitTests/BackgroundTests.cs index b13a4d3..496d7d4 100644 --- a/QuestPDF.UnitTests/BackgroundTests.cs +++ b/QuestPDF.UnitTests/BackgroundTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/BorderTests.cs b/QuestPDF.UnitTests/BorderTests.cs index d73bcda..2a98a1a 100644 --- a/QuestPDF.UnitTests/BorderTests.cs +++ b/QuestPDF.UnitTests/BorderTests.cs @@ -2,7 +2,7 @@ using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/ConstrainedTests.cs b/QuestPDF.UnitTests/ConstrainedTests.cs index 9b3dbf0..81c4432 100644 --- a/QuestPDF.UnitTests/ConstrainedTests.cs +++ b/QuestPDF.UnitTests/ConstrainedTests.cs @@ -2,7 +2,7 @@ using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/DebugTests.cs b/QuestPDF.UnitTests/DebugTests.cs index 97d9662..b81c1f5 100644 --- a/QuestPDF.UnitTests/DebugTests.cs +++ b/QuestPDF.UnitTests/DebugTests.cs @@ -1,6 +1,6 @@ using NUnit.Framework; using QuestPDF.Elements; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/ExtendTests.cs b/QuestPDF.UnitTests/ExtendTests.cs index a523c62..8f479f8 100644 --- a/QuestPDF.UnitTests/ExtendTests.cs +++ b/QuestPDF.UnitTests/ExtendTests.cs @@ -1,6 +1,6 @@ using NUnit.Framework; using QuestPDF.Elements; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/Helpers.cs b/QuestPDF.UnitTests/Helpers.cs index b12db9e..13e5efa 100644 --- a/QuestPDF.UnitTests/Helpers.cs +++ b/QuestPDF.UnitTests/Helpers.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { @@ -13,7 +13,7 @@ namespace QuestPDF.UnitTests public static Size RandomSize => new Size(Random.Next(200, 400), Random.Next(100, 200)); - public static void ShouldEqual(this IEnumerable commands, IEnumerable expected) + public static void ShouldEqual(this IEnumerable commands, IEnumerable expected) { commands.Should().HaveSameCount(expected); diff --git a/QuestPDF.UnitTests/PaddingTests.cs b/QuestPDF.UnitTests/PaddingTests.cs index 2bd9946..1a79734 100644 --- a/QuestPDF.UnitTests/PaddingTests.cs +++ b/QuestPDF.UnitTests/PaddingTests.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/ShowOnceTest.cs b/QuestPDF.UnitTests/ShowOnceTest.cs index c87ed7b..f7edb86 100644 --- a/QuestPDF.UnitTests/ShowOnceTest.cs +++ b/QuestPDF.UnitTests/ShowOnceTest.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { diff --git a/QuestPDF.UnitTests/StackTests.cs b/QuestPDF.UnitTests/StackTests.cs index 3fea63f..8f38cb8 100644 --- a/QuestPDF.UnitTests/StackTests.cs +++ b/QuestPDF.UnitTests/StackTests.cs @@ -2,7 +2,7 @@ using QuestPDF.Drawing.SpacePlan; using QuestPDF.Elements; using QuestPDF.Infrastructure; -using QuestPDF.UnitTests.MeasureTest; +using QuestPDF.UnitTests.TestEngine; namespace QuestPDF.UnitTests { @@ -220,6 +220,9 @@ namespace QuestPDF.UnitTests .ExpectCanvasTranslate(0, -250) .ExpectChildMeasure("c", expectedInput: new Size(500, 400), returns: new FullRender(Size.Zero)) + .ExpectCanvasTranslate(0, 600) + .ExpectChildDraw("c", new Size(500, 0)) + .ExpectCanvasTranslate(0, -600) .ExpectChildMeasure("d", expectedInput: new Size(500, 400), returns: new FullRender(200, 400)) .ExpectCanvasTranslate(0, 600) diff --git a/QuestPDF.UnitTests/TestEngine/CanvasMock.cs b/QuestPDF.UnitTests/TestEngine/CanvasMock.cs new file mode 100644 index 0000000..fa0081f --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/CanvasMock.cs @@ -0,0 +1,43 @@ +using System; +using QuestPDF.Infrastructure; +using SkiaSharp; + +namespace QuestPDF.UnitTests.TestEngine +{ + internal class CanvasMock : ICanvas + { + public Action TranslateFunc { get; set; } + public Action DrawImageFunc { get; set; } + public Action DrawTextFunc { get; set; } + public Action DrawRectFunc { get; set; } + + public void Translate(Position vector) => TranslateFunc(vector); + public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color); + 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); + public void DrawExternalLink(string url, Size size) + { + throw new NotImplementedException(); + } + + public void DrawLocationLink(string locationName, Size size) + { + throw new NotImplementedException(); + } + + public void DrawLocation(string locationName) + { + throw new NotImplementedException(); + } + + public void DrawLink(string url, Size size) + { + throw new NotImplementedException(); + } + + public Size MeasureText(string text, TextStyle style) + { + return new Size(text.Length * style.Size, style.Size); + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/ElementMock.cs b/QuestPDF.UnitTests/TestEngine/ElementMock.cs new file mode 100644 index 0000000..651a418 --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/ElementMock.cs @@ -0,0 +1,16 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine +{ + internal class ElementMock : Element + { + public string Id { get; set; } + public Func MeasureFunc { get; set; } + public Action DrawFunc { get; set; } + + internal override ISpacePlan Measure(Size availableSpace) => MeasureFunc(availableSpace); + internal override void Draw(ICanvas canvas, Size availableSpace) => DrawFunc(availableSpace); + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/OperationBase.cs b/QuestPDF.UnitTests/TestEngine/OperationBase.cs new file mode 100644 index 0000000..1f1660b --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/OperationBase.cs @@ -0,0 +1,7 @@ +namespace QuestPDF.UnitTests.TestEngine +{ + public abstract class OperationBase + { + + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs new file mode 100644 index 0000000..7f0bdde --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawImageOperation.cs @@ -0,0 +1,16 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + internal class CanvasDrawImageOperationBase : OperationBase + { + public Position Position { get; } + public Size Size { get; } + + public CanvasDrawImageOperationBase(Position position, Size size) + { + Position = position; + Size = size; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs new file mode 100644 index 0000000..a648b2c --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs @@ -0,0 +1,18 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + internal class CanvasDrawRectangleOperationBase : OperationBase + { + public Position Position { get; } + public Size Size { get; } + public string Color { get; } + + public CanvasDrawRectangleOperationBase(Position position, Size size, string color) + { + Position = position; + Size = size; + Color = color; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs new file mode 100644 index 0000000..74b2b60 --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawTextOperation.cs @@ -0,0 +1,18 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + internal class CanvasDrawTextOperationBase : OperationBase + { + public string Text { get; } + public Position Position { get; } + public TextStyle Style { get; } + + public CanvasDrawTextOperationBase(string text, Position position, TextStyle style) + { + Text = text; + Position = position; + Style = style; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs new file mode 100644 index 0000000..0e387a7 --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/CanvasTranslateOperation.cs @@ -0,0 +1,14 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + internal class CanvasTranslateOperationBase : OperationBase + { + public Position Position { get; } + + public CanvasTranslateOperationBase(Position position) + { + Position = position; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs new file mode 100644 index 0000000..d1928e0 --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/ChildDrawOperation.cs @@ -0,0 +1,16 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + public class ChildDrawOperationBase : OperationBase + { + public string ChildId { get; } + public Size Input { get; } + + public ChildDrawOperationBase(string childId, Size input) + { + ChildId = childId; + Input = input; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs new file mode 100644 index 0000000..323c2dd --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/ChildMeasureOperation.cs @@ -0,0 +1,19 @@ +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + internal class ChildMeasureOperationBase : OperationBase + { + public string ChildId { get; } + public Size Input { get; } + public ISpacePlan Output { get; } + + public ChildMeasureOperationBase(string childId, Size input, ISpacePlan output) + { + ChildId = childId; + Input = input; + Output = output; + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs b/QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs new file mode 100644 index 0000000..8e4df23 --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/Operations/ElementMeasureOperation.cs @@ -0,0 +1,12 @@ +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine.Operations +{ + public class ElementMeasureOperationBase : OperationBase + { + public ElementMeasureOperationBase(Size input) + { + + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/SingleChildTests.cs b/QuestPDF.UnitTests/TestEngine/SingleChildTests.cs new file mode 100644 index 0000000..507dcca --- /dev/null +++ b/QuestPDF.UnitTests/TestEngine/SingleChildTests.cs @@ -0,0 +1,31 @@ +using FluentAssertions; +using NUnit.Framework; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.UnitTests.TestEngine +{ + public static class SingleChildTests + { + internal static void MeasureWithoutChild(this T element) where T : ContainerElement + { + element.Child = null; + element.Measure(Size.Zero).Should().BeEquivalentTo(new FullRender(Size.Zero)); + } + + internal static void DrawWithoutChild(this T element) where T : ContainerElement + { + // component does not throw an exception when called with null child + Assert.DoesNotThrow(() => + { + element.Child = null; + + // component does not perform any canvas operation when called with null child + TestPlan + .For(x => element) + .DrawElement(new Size(200, 100)) + .CheckDrawResult(); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/MeasureTest/MeasureTestMock.cs b/QuestPDF.UnitTests/TestEngine/TestPlan.cs similarity index 50% rename from QuestPDF.UnitTests/MeasureTest/MeasureTestMock.cs rename to QuestPDF.UnitTests/TestEngine/TestPlan.cs index 6ea6007..9cf72c2 100644 --- a/QuestPDF.UnitTests/MeasureTest/MeasureTestMock.cs +++ b/QuestPDF.UnitTests/TestEngine/TestPlan.cs @@ -1,144 +1,20 @@ using System; using System.Collections.Generic; using System.Text.Json; -using FluentAssertions; using NUnit.Framework; using QuestPDF.Drawing.SpacePlan; -using QuestPDF.Elements; using QuestPDF.Infrastructure; -using SkiaSharp; +using QuestPDF.UnitTests.TestEngine.Operations; -namespace QuestPDF.UnitTests.MeasureTest +namespace QuestPDF.UnitTests.TestEngine { - public abstract class Operation - { - - } - - public class ElementMeasureOperation : Operation - { - public ElementMeasureOperation(Size input) - { - - } - } - - internal class ChildMeasureOperation : Operation - { - public string ChildId { get; } - public Size Input { get; } - public ISpacePlan Output { get; } - - public ChildMeasureOperation(string childId, Size input, ISpacePlan output) - { - ChildId = childId; - Input = input; - Output = output; - } - } - - public class ChildDrawOperation : Operation - { - public string ChildId { get; } - public Size Input { get; } - - public ChildDrawOperation(string childId, Size input) - { - ChildId = childId; - Input = input; - } - } - - internal class CanvasTranslateOperation : Operation - { - public Position Position { get; } - - public CanvasTranslateOperation(Position position) - { - Position = position; - } - } - - internal class CanvasDrawRectangleOperation : Operation - { - public Position Position { get; } - public Size Size { get; } - public string Color { get; } - - public CanvasDrawRectangleOperation(Position position, Size size, string color) - { - Position = position; - Size = size; - Color = color; - } - } - - internal class CanvasDrawTextOperation : Operation - { - public string Text { get; } - public Position Position { get; } - public TextStyle Style { get; } - - public CanvasDrawTextOperation(string text, Position position, TextStyle style) - { - Text = text; - Position = position; - Style = style; - } - } - - internal class CanvasDrawImageOperation : Operation - { - public Position Position { get; } - public Size Size { get; } - - public CanvasDrawImageOperation(Position position, Size size) - { - Position = position; - Size = size; - } - } - - internal class ElementMock : Element - { - public string Id { get; set; } - public Func MeasureFunc { get; set; } - public Action DrawFunc { get; set; } - - internal override ISpacePlan Measure(Size availableSpace) => MeasureFunc(availableSpace); - internal override void Draw(ICanvas canvas, Size availableSpace) => DrawFunc(availableSpace); - } - - internal class CanvasMock : ICanvas - { - public Action TranslateFunc { get; set; } - public Action DrawImageFunc { get; set; } - public Action DrawTextFunc { get; set; } - public Action DrawRectFunc { get; set; } - - public void Translate(Position vector) => TranslateFunc(vector); - public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color); - 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); - - public void DrawLink(string url, Size size) - { - throw new NotImplementedException(); - } - - public Size MeasureText(string text, TextStyle style) - { - return new Size(text.Length * style.Size, style.Size); - } - } - internal class TestPlan { private Element Element { get; set; } private ICanvas Canvas { get; } private Size OperationInput { get; set; } - private Queue Operations { get; } = new Queue(); + private Queue Operations { get; } = new Queue(); public TestPlan() { @@ -153,7 +29,7 @@ namespace QuestPDF.UnitTests.MeasureTest return plan; } - private T GetExpected() where T : Operation + private T GetExpected() where T : OperationBase { if (Operations.TryDequeue(out var value) && value is T result) return result; @@ -169,7 +45,7 @@ namespace QuestPDF.UnitTests.MeasureTest { TranslateFunc = position => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.Position.X, position.X); Assert.AreEqual(expected.Position.Y, position.Y); @@ -178,7 +54,7 @@ namespace QuestPDF.UnitTests.MeasureTest }, DrawRectFunc = (position, size, color) => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.Position.X, position.X); Assert.AreEqual(expected.Position.Y, position.Y); @@ -194,7 +70,7 @@ namespace QuestPDF.UnitTests.MeasureTest }, DrawTextFunc = (text, position, style) => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.Text, text); @@ -211,7 +87,7 @@ namespace QuestPDF.UnitTests.MeasureTest }, DrawImageFunc = (image, position, size) => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.Position.X, position.X); Assert.AreEqual(expected.Position.Y, position.Y); @@ -232,7 +108,7 @@ namespace QuestPDF.UnitTests.MeasureTest Id = id, MeasureFunc = availableSpace => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.ChildId, id); @@ -246,7 +122,7 @@ namespace QuestPDF.UnitTests.MeasureTest }, DrawFunc = availableSpace => { - var expected = GetExpected(); + var expected = GetExpected(); Assert.AreEqual(expected.ChildId, id); @@ -271,45 +147,45 @@ namespace QuestPDF.UnitTests.MeasureTest return this; } - private TestPlan AddOperation(Operation operation) + private TestPlan AddOperation(OperationBase operationBase) { - Operations.Enqueue(operation); + Operations.Enqueue(operationBase); return this; } public TestPlan ExpectChildMeasure(string child, Size expectedInput, ISpacePlan returns) { - return AddOperation(new ChildMeasureOperation(child, expectedInput, returns)); + return AddOperation(new ChildMeasureOperationBase(child, expectedInput, returns)); } public TestPlan ExpectChildDraw(string child, Size expectedInput) { - return AddOperation(new ChildDrawOperation(child, expectedInput)); + return AddOperation(new ChildDrawOperationBase(child, expectedInput)); } public TestPlan ExpectCanvasTranslate(Position position) { - return AddOperation(new CanvasTranslateOperation(position)); + return AddOperation(new CanvasTranslateOperationBase(position)); } public TestPlan ExpectCanvasTranslate(float left, float top) { - return AddOperation(new CanvasTranslateOperation(new Position(left, top))); + return AddOperation(new CanvasTranslateOperationBase(new Position(left, top))); } public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, string color) { - return AddOperation(new CanvasDrawRectangleOperation(position, size, color)); + return AddOperation(new CanvasDrawRectangleOperationBase(position, size, color)); } public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style) { - return AddOperation(new CanvasDrawTextOperation(text, position, style)); + return AddOperation(new CanvasDrawTextOperationBase(text, position, style)); } public TestPlan ExpectCanvasDrawImage(Position position, Size size) { - return AddOperation(new CanvasDrawImageOperation(position, size)); + return AddOperation(new CanvasDrawImageOperationBase(position, size)); } public TestPlan CheckMeasureResult(ISpacePlan expected) @@ -336,70 +212,4 @@ namespace QuestPDF.UnitTests.MeasureTest return this; } } - - public static class SingleChildTests - { - internal static void MeasureWithoutChild(this T element) where T : ContainerElement - { - element.Child = null; - element.Measure(Size.Zero).Should().BeEquivalentTo(new FullRender(Size.Zero)); - } - - internal static void DrawWithoutChild(this T element) where T : ContainerElement - { - // component does not throw an exception when called with null child - Assert.DoesNotThrow(() => - { - element.Child = null; - - // component does not perform any canvas operation when called with null child - TestPlan - .For(x => element) - .DrawElement(new Size(200, 100)) - .CheckDrawResult(); - }); - } - } - - [TestFixture] - public class LetsTest - { - [Test] - public void TestExample() - { - TestPlan - .For(x => new Padding - { - Top = 5, - Right = 10, - Bottom = 15, - Left = 20, - - Child = x.CreateChild("child") - }) - .MeasureElement(new Size(200, 100)) - .ExpectChildMeasure("child", expectedInput: new Size(170, 80), returns: new FullRender(new Size(100, 50))) - .CheckMeasureResult(new FullRender(130, 70)); - } - - [Test] - public void TestExample2() - { - TestPlan - .For(x => new Padding - { - Top = 5, - Right = 10, - Bottom = 15, - Left = 20, - - Child = x.CreateChild("child") - }) - .DrawElement(new Size(200, 100)) - .ExpectCanvasTranslate(new Position(20, 5)) - .ExpectChildDraw("child", expectedInput: new Size(170, 80)) - .ExpectCanvasTranslate(new Position(-20, -5)) - .CheckDrawResult(); - } - } } \ No newline at end of file diff --git a/QuestPDF/Drawing/Canvas.cs b/QuestPDF/Drawing/Canvas.cs index 46bf8f9..7c54e36 100644 --- a/QuestPDF/Drawing/Canvas.cs +++ b/QuestPDF/Drawing/Canvas.cs @@ -41,9 +41,19 @@ namespace QuestPDF.Drawing SkiaCanvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height)); } - public void DrawLink(string url, Size size) + public void DrawExternalLink(string url, Size size) { SkiaCanvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url); } + + public void DrawLocationLink(string locationName, Size size) + { + SkiaCanvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName); + } + + public void DrawLocation(string locationName) + { + SkiaCanvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName); + } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/CanvasCache.cs b/QuestPDF/Drawing/CanvasCache.cs index 8319e27..2b37593 100644 --- a/QuestPDF/Drawing/CanvasCache.cs +++ b/QuestPDF/Drawing/CanvasCache.cs @@ -30,13 +30,15 @@ namespace QuestPDF.Drawing static SKPaint Convert(TextStyle style) { + var slant = style.IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; + return new SKPaint { Color = SKColor.Parse(style.Color), - Typeface = Fonts.GetOrAdd(style.FontType, SKTypeface.FromFamilyName), + Typeface = SKTypeface.FromFamilyName(style.FontType, (int)style.FontWeight, (int)SKFontStyleWidth.Normal, slant), TextSize = style.Size, - IsLinearText = true, - + TextEncoding = SKTextEncoding.Utf32, + TextAlign = style.Alignment switch { HorizontalAlignment.Left => SKTextAlign.Left, diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 2578554..89221ba 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -12,7 +12,6 @@ namespace QuestPDF.Drawing static class DocumentGenerator { const int DocumentLayoutExceptionThreshold = 250; - private static readonly Watermark Watermark = new Watermark(); internal static void Generate(Stream stream, IDocument document) { diff --git a/QuestPDF/Elements/ExternalLink.cs b/QuestPDF/Elements/ExternalLink.cs new file mode 100644 index 0000000..2a7c72a --- /dev/null +++ b/QuestPDF/Elements/ExternalLink.cs @@ -0,0 +1,21 @@ +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class ExternalLink : ContainerElement + { + public string Url { get; set; } = "https://www.questpdf.com"; + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var targetSize = Child?.Measure(availableSpace) as Size; + + if (targetSize == null) + return; + + canvas.DrawExternalLink(Url, targetSize); + Child?.Draw(canvas, availableSpace); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/InternalLink.cs b/QuestPDF/Elements/InternalLink.cs new file mode 100644 index 0000000..da95a6e --- /dev/null +++ b/QuestPDF/Elements/InternalLink.cs @@ -0,0 +1,21 @@ +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class InternalLink : ContainerElement + { + public string LocationName { get; set; } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + var targetSize = Child?.Measure(availableSpace) as Size; + + if (targetSize == null) + return; + + canvas.DrawLocationLink(LocationName, targetSize); + Child?.Draw(canvas, availableSpace); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/InternalLocation.cs b/QuestPDF/Elements/InternalLocation.cs new file mode 100644 index 0000000..d4a64ed --- /dev/null +++ b/QuestPDF/Elements/InternalLocation.cs @@ -0,0 +1,17 @@ +using System; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class InternalLocation : ContainerElement + { + public string LocationName { get; set; } + + internal override void Draw(ICanvas canvas, Size availableSpace) + { + canvas.DrawLocation(LocationName); + Child?.Draw(canvas, availableSpace); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Stack.cs b/QuestPDF/Elements/Stack.cs index e82ed63..6ebad06 100644 --- a/QuestPDF/Elements/Stack.cs +++ b/QuestPDF/Elements/Stack.cs @@ -47,10 +47,8 @@ namespace QuestPDF.Elements var size = space as Size; - if (size.Height < Size.Epsilon) - continue; - - heightOnCurrentPage += size.Height + Spacing; + if (size.Height > Size.Epsilon) + heightOnCurrentPage += size.Height + Spacing; if (space is PartialRender) { @@ -81,18 +79,13 @@ namespace QuestPDF.Elements break; var size = space as Size; - - if (size.Height < Size.Epsilon) - { - ChildrenQueue.Dequeue(); - continue; - } - + canvas.Translate(new Position(0, topOffset)); child.Draw(canvas, new Size(availableSpace.Width, size.Height)); canvas.Translate(new Position(0, -topOffset)); - topOffset += size.Height + Spacing; + if (size.Height > Size.Epsilon) + topOffset += size.Height + Spacing; if (space is PartialRender) break; diff --git a/QuestPDF/Elements/Watermark.cs b/QuestPDF/Elements/Watermark.cs deleted file mode 100644 index fd8ccfc..0000000 --- a/QuestPDF/Elements/Watermark.cs +++ /dev/null @@ -1,62 +0,0 @@ -using QuestPDF.Drawing.SpacePlan; -using QuestPDF.Infrastructure; - -namespace QuestPDF.Elements -{ - internal class Watermark : Element - { - private Position Offset { get; set; } = new Position(36, 36); - private const float ImageHeight = 28; - private const string TargetUrl = "https://www.questpdf.com"; - - private Image Image { get; set; } - private static readonly byte[] ImageData; - - static Watermark() - { - ImageData = Helpers.Helpers.LoadEmbeddedResource("QuestPDF.Resources.Watermark.png"); - } - - public Watermark() - { - Image = new Image() - { - Data = ImageData - }; - } - - internal void AdjustPosition(Element? element) - { - while (element != null) - { - if (element is Padding padding) - { - if (padding.Left > 0 && padding.Bottom > 0) - Offset = new Position(padding.Left, padding.Bottom); - - return; - } - - element = (element as ContainerElement)?.Child; - } - } - - internal override ISpacePlan Measure(Size availableSpace) - { - return Image.Measure(availableSpace); - } - - internal override void Draw(ICanvas canvas, Size availableSpace) - { - var offset = new Position(Offset.X, availableSpace.Height - Offset.Y - ImageHeight); - canvas.Translate(offset); - - availableSpace = new Size(availableSpace.Width, ImageHeight); - var targetSize = Image.Measure(availableSpace) as FullRender; - Image.Draw(canvas, targetSize); - canvas.DrawLink(TargetUrl, targetSize); - - canvas.Translate(offset.Reverse()); - } - } -} \ No newline at end of file diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index 902907c..e03504a 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -66,6 +66,14 @@ namespace QuestPDF.Fluent }); } + public static void DynamicImage(this IContainer element, Func imageSource) + { + element.Element(new DynamicImage + { + Source = imageSource + }); + } + public static void PageNumber(this IContainer element, string textFormat = "{number}", TextStyle? style = null) { element.Element(new PageNumber @@ -127,11 +135,27 @@ namespace QuestPDF.Fluent return element.Element(new Container()); } - private static void DynamicImage(this IContainer element, Func handler) + public static IContainer ExternalLink(this IContainer element, string url) { - element.Element(new DynamicImage() + return element.Element(new ExternalLink { - Source = handler + Url = url + }); + } + + public static IContainer Location(this IContainer element, string locationName) + { + return element.Element(new InternalLocation + { + LocationName = locationName + }); + } + + public static IContainer InternalLink(this IContainer element, string locationName) + { + return element.Element(new InternalLink + { + LocationName = locationName }); } } diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 7aa4c03..0acf5d4 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -31,6 +31,13 @@ namespace QuestPDF.Fluent return style.Mutate(x => x.LineHeight = value); } + public static TextStyle Italic(this TextStyle style, bool value = true) + { + return style.Mutate(x => x.IsItalic = value); + } + + #region Alignmnet + public static TextStyle Alignment(this TextStyle style, HorizontalAlignment value) { return style.Mutate(x => x.Alignment = value); @@ -38,17 +45,78 @@ namespace QuestPDF.Fluent public static TextStyle AlignLeft(this TextStyle style) { - return style.Mutate(x => x.Alignment = HorizontalAlignment.Left); + return style.Alignment(HorizontalAlignment.Left); } public static TextStyle AlignCenter(this TextStyle style) { - return style.Mutate(x => x.Alignment = HorizontalAlignment.Center); + return style.Alignment(HorizontalAlignment.Center); } public static TextStyle AlignRight(this TextStyle style) { - return style.Mutate(x => x.Alignment = HorizontalAlignment.Right); + return style.Alignment(HorizontalAlignment.Right); } + + #endregion + + #region Weight + + public static TextStyle Weight(this TextStyle style, FontWeight weight) + { + return style.Mutate(x => x.FontWeight = weight); + } + + public static TextStyle Thin(this TextStyle style) + { + return style.Weight(FontWeight.Thin); + } + + public static TextStyle ExtraLight(this TextStyle style) + { + return style.Weight(FontWeight.ExtraLight); + } + + public static TextStyle Light(this TextStyle style) + { + return style.Weight(FontWeight.Light); + } + + public static TextStyle NormalWeight(this TextStyle style) + { + return style.Weight(FontWeight.Normal); + } + + public static TextStyle Medium(this TextStyle style) + { + return style.Weight(FontWeight.Medium); + } + + public static TextStyle SemiBold(this TextStyle style) + { + return style.Weight(FontWeight.SemiBold); + } + + public static TextStyle Bold(this TextStyle style) + { + return style.Weight(FontWeight.Bold); + } + + public static TextStyle ExtraBold(this TextStyle style) + { + return style.Weight(FontWeight.ExtraBold); + } + + public static TextStyle Black(this TextStyle style) + { + return style.Weight(FontWeight.Black); + } + + public static TextStyle ExtraBlack(this TextStyle style) + { + return style.Weight(FontWeight.ExtraBlack); + } + + #endregion } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/FontWeight.cs b/QuestPDF/Infrastructure/FontWeight.cs new file mode 100644 index 0000000..2b50c39 --- /dev/null +++ b/QuestPDF/Infrastructure/FontWeight.cs @@ -0,0 +1,16 @@ +namespace QuestPDF.Infrastructure +{ + public enum FontWeight + { + Thin = 100, + ExtraLight = 200, + Light = 300, + Normal = 400, + Medium = 500, + SemiBold = 600, + Bold = 700, + ExtraBold = 800, + Black = 900, + ExtraBlack = 1000 + } +} diff --git a/QuestPDF/Infrastructure/ICanvas.cs b/QuestPDF/Infrastructure/ICanvas.cs index a94a18c..f5a5ec9 100644 --- a/QuestPDF/Infrastructure/ICanvas.cs +++ b/QuestPDF/Infrastructure/ICanvas.cs @@ -10,6 +10,8 @@ namespace QuestPDF.Infrastructure void DrawText(string text, Position position, TextStyle style); void DrawImage(SKImage image, Position position, Size size); - void DrawLink(string url, Size size); + void DrawExternalLink(string url, Size size); + void DrawLocationLink(string locationName, Size size); + void DrawLocation(string locationName); } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 70b989f..e01b136 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -2,17 +2,19 @@ { public class TextStyle { - public string Color { get; set; } = "#000000"; - public string FontType { get; set; } = "Helvetica"; - public float Size { get; set; } = 12; - public float LineHeight { get; set; } = 1.2f; - public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; - + internal string Color { get; set; } = "#000000"; + internal string FontType { get; set; } = "Helvetica"; + internal float Size { get; set; } = 12; + internal float LineHeight { get; set; } = 1.2f; + internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; + internal FontWeight FontWeight { get; set; } = FontWeight.Normal; + internal bool IsItalic { get; set; } = false; + public static TextStyle Default => new TextStyle(); public override string ToString() { - return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}"; + return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}"; } } } \ No newline at end of file diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index e0f98f3..cdec3ac 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,9 +4,9 @@ MarcinZiabek CodeFlint QuestPDF - 2021.1.0 + 2021.2.0 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. - The library is open-source and totally free now. Removed watermak. + 2021.2.0 Internal links, external links, dynamic images, font weights 8 true Logo.png