diff --git a/QuestPDF.Examples/DynamicExamples.cs b/QuestPDF.Examples/DynamicExamples.cs new file mode 100644 index 0000000..8e94e33 --- /dev/null +++ b/QuestPDF.Examples/DynamicExamples.cs @@ -0,0 +1,185 @@ +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using QuestPDF.Elements; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class OrderItem + { + public string ItemName { get; set; } = Placeholders.Label(); + public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10; + public int Count { get; set; } = Placeholders.Random.Next(1, 11); + } + + public struct OrdersTableState + { + public int ShownItemsCount { get; set; } + } + + public class OrdersTable : IDynamicComponent + { + private ICollection Items { get; } + public OrdersTableState State { get; set; } + + public OrdersTable(ICollection items) + { + Items = items; + + State = new OrdersTableState + { + ShownItemsCount = 0 + }; + } + + public DynamicComponentComposeResult Compose(DynamicContext context) + { + var header = ComposeHeader(context); + var sampleFooter = ComposeFooter(context, Enumerable.Empty()); + var decorationHeight = header.Size.Height + sampleFooter.Size.Height; + + var rows = GetItemsForPage(context, decorationHeight).ToList(); + var footer = ComposeFooter(context, rows.Select(x => x.Item)); + + var content = context.CreateElement(container => + { + container.MinimalBox().Decoration(decoration => + { + decoration.Header().Element(header); + + decoration.Content().Box().Stack(stack => + { + foreach (var row in rows) + stack.Item().Element(row.Element); + }); + + decoration.Footer().Element(footer); + }); + }); + + State = new OrdersTableState + { + ShownItemsCount = State.ShownItemsCount + rows.Count + }; + + return new DynamicComponentComposeResult + { + Content = content, + HasMoreContent = State.ShownItemsCount + rows.Count < Items.Count + }; + } + + private IDynamicElement ComposeHeader(DynamicContext context) + { + return context.CreateElement(element => + { + element + .Width(context.AvailableSize.Width) + .BorderBottom(1) + .BorderColor(Colors.Grey.Darken2) + .Padding(5) + .Row(row => + { + var textStyle = TextStyle.Default.SemiBold(); + + row.ConstantItem(30).Text("#", textStyle); + row.RelativeItem().Text("Item name", textStyle); + row.ConstantItem(50).AlignRight().Text("Count", textStyle); + row.ConstantItem(50).AlignRight().Text("Price", textStyle); + row.ConstantItem(50).AlignRight().Text("Total", textStyle); + }); + }); + } + + private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable items) + { + var total = items.Sum(x => x.Count * x.Price); + + return context.CreateElement(element => + { + element + .Width(context.AvailableSize.Width) + .Padding(5) + .AlignRight() + .Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold()); + }); + } + + private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight) + { + var totalHeight = decorationHeight; + + foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount)) + { + var item = Items.ElementAt(index); + + var element = context.CreateElement(content => + { + content + .Width(context.AvailableSize.Width) + .BorderBottom(1) + .BorderColor(Colors.Grey.Lighten2) + .Padding(5) + .Row(row => + { + row.ConstantItem(30).Text(index + 1); + row.RelativeItem().Text(item.ItemName); + row.ConstantItem(50).AlignRight().Text(item.Count); + row.ConstantItem(50).AlignRight().Text($"{item.Price}$"); + row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$"); + }); + }); + + var elementHeight = element.Size.Height; + + if (totalHeight + elementHeight > context.AvailableSize.Height) + break; + + totalHeight += elementHeight; + yield return (item, element); + } + } + } + + public static class DynamicExamples + { + [Test] + public static void Dynamic() + { + RenderingTest + .Create() + .PageSize(PageSizes.A5) + .ShowResults() + .Render(container => + { + var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList(); + + container + .Background(Colors.White) + .Padding(25) + .Decoration(decoration => + { + decoration + .Header() + .PaddingBottom(5) + .Text(text => + { + text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16)); + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" of "); + text.TotalPages(); + }); + + decoration + .Content() + .Dynamic(new OrdersTable(items)); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/TableBenchmark.cs b/QuestPDF.Examples/TableBenchmark.cs new file mode 100644 index 0000000..9cba9f3 --- /dev/null +++ b/QuestPDF.Examples/TableBenchmark.cs @@ -0,0 +1,46 @@ +using System.Linq; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; + +namespace QuestPDF.Examples +{ + public class TableBenchmark + { + [Test] + public void Benchmark() + { + RenderingTest + .Create() + .ProducePdf() + .PageSize(PageSizes.A4) + .ShowResults() + .MaxPages(10_000) + .EnableCaching(true) + .EnableDebugging(false) + .Render(container => + { + container + .Padding(10) + .MinimalBox() + .Border(1) + .Table(table => + { + const int numberOfRows = 100_000; + const int numberOfColumns = 10; + + table.ColumnsDefinition(columns => + { + foreach (var _ in Enumerable.Range(0, numberOfColumns)) + columns.RelativeColumn(); + }); + + foreach (var row in Enumerable.Range(0, numberOfRows)) + foreach (var column in Enumerable.Range(0, numberOfColumns)) + table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}"); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 1b26a24..d0d05ba 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -36,7 +36,99 @@ namespace QuestPDF.Examples }); }); } + + [Test] + public void SuperscriptSubscript_General() + { + RenderingTest + .Create() + .PageSize(500, 500) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(5) + .MinimalBox() + .Border(1) + .Padding(10) + .Text(text => + { + text.DefaultTextStyle(x => x.FontSize(20)); + text.ParagraphSpacing(2); + + + text.Span("In physics, mass–energy equivalence is the relationship between mass and energy in a system's rest frame, where the two values differ only by a constant and the units of measurement."); + text.Span("[1][2]").Superscript(); + text.Span(" The principle is described by the physicist Albert Einstein's famous formula: E = mc"); + text.Span("2").Superscript(); + text.Span(". "); + text.Span("[3]").Superscript(); + + text.EmptyLine(); + + text.Span("H"); + text.Span("2").Subscript(); + text.Span("O is the chemical formula for water, meaning that each of its molecules contains one oxygen and two hydrogen atoms."); + + text.EmptyLine(); + + text.Span("H"); + text.Span("2").Subscript(); + text.Span("O"); + }); + }); + } + [Test] + public void SuperscriptSubscript_Effects() + { + RenderingTest + .Create() + .PageSize(800, 400) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(25) + .DefaultTextStyle(x => x.FontSize(30)) + .Column(column => + { + column.Spacing(25); + + column.Item().Text(text => + { + text.DefaultTextStyle(x => x.Underline()); + + text.Span("Underline of the superscript (E = mc"); + text.Span("2").Superscript(); + text.Span(") should be at the same height as for normal text."); + }); + + column.Item().Text(text => + { + text.DefaultTextStyle(x => x.Underline()); + + text.Span("Underline of the subscript(H"); + text.Span("2").Subscript(); + text.Span("O) should be slightly lower than a normal text."); + }); + + column.Item().Text(text => + { + text.DefaultTextStyle(x => x.Strikethrough()); + + text.Span("Strikethrough of both superscript (E=mc"); + text.Span("2").Superscript(); + text.Span(") and subscript(H"); + text.Span("2").Subscript(); + text.Span("O) should be visible in the middle of the text."); + }); + }); + }); + } + [Test] public void ParagraphSpacing() { @@ -397,7 +489,7 @@ namespace QuestPDF.Examples text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24)); text.Span(" " + Placeholders.LoremIpsum()); - text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere(); + text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere(); }); }); }); diff --git a/QuestPDF.Previewer/Helpers.cs b/QuestPDF.Previewer/Helpers.cs index fe0a809..b01c2f3 100644 --- a/QuestPDF.Previewer/Helpers.cs +++ b/QuestPDF.Previewer/Helpers.cs @@ -8,16 +8,13 @@ class Helpers { using var stream = File.Create(filePath); - var document = SKDocument.CreatePdf(stream); + using var document = SKDocument.CreatePdf(stream); foreach (var page in pages) { using var canvas = document.BeginPage(page.Width, page.Height); canvas.DrawPicture(page.Picture); document.EndPage(); - canvas.Dispose(); } - - document.Close(); } } \ No newline at end of file diff --git a/QuestPDF.Previewer/PreviewerWindowViewModel.cs b/QuestPDF.Previewer/PreviewerWindowViewModel.cs index 4246ed5..fa353c9 100644 --- a/QuestPDF.Previewer/PreviewerWindowViewModel.cs +++ b/QuestPDF.Previewer/PreviewerWindowViewModel.cs @@ -63,7 +63,7 @@ namespace QuestPDF.Previewer private void OpenLink(string path) { - var openBrowserProcess = new Process + using var openBrowserProcess = new Process { StartInfo = new() { diff --git a/QuestPDF.Previewer/QuestPDF.Previewer.csproj b/QuestPDF.Previewer/QuestPDF.Previewer.csproj index eb8f0e9..5aeb9fa 100644 --- a/QuestPDF.Previewer/QuestPDF.Previewer.csproj +++ b/QuestPDF.Previewer/QuestPDF.Previewer.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF.Previewer - 2022.4.1 + 2022.5.0 true questpdf-previewer 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. diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 6e43689..cd9b339 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -164,7 +164,7 @@ namespace QuestPDF.Drawing return debuggingState; } - private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) + internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle) { if (content == null) return; @@ -186,6 +186,9 @@ namespace QuestPDF.Drawing return; } + if (content is DynamicHost dynamicHost) + dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); + var targetTextStyle = documentDefaultTextStyle; if (content is DefaultTextStyle defaultTextStyleElement) diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 98688a4..3c699a9 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.IO; using System.Linq; +using QuestPDF.Fluent; using QuestPDF.Infrastructure; using SkiaSharp; @@ -68,7 +69,7 @@ namespace QuestPDF.Drawing { Color = SKColor.Parse(style.Color), Typeface = GetTypeface(style), - TextSize = style.Size ?? 12, + TextSize = (style.Size ?? 12) * GetTextScale(style), IsAntialias = true, }; } @@ -76,6 +77,16 @@ namespace QuestPDF.Drawing static SKTypeface GetTypeface(TextStyle style) { var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal); + + // superscript and subscript use slightly bolder font to match visually line thickness + if (style.FontPosition is FontPosition.Superscript or FontPosition.Subscript) + { + var weightValue = (int)weight; + weightValue = Math.Min(weightValue + 100, 1000); + + weight = (SKFontStyleWeight) (weightValue); + } + var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant); @@ -94,11 +105,22 @@ namespace QuestPDF.Drawing $"1) install the font on your operating system or execution environment. " + $"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method."); } + + static float GetTextScale(TextStyle style) + { + return style.FontPosition switch + { + FontPosition.Normal => 1f, + FontPosition.Subscript => 0.625f, + FontPosition.Superscript => 0.625f, + _ => throw new ArgumentOutOfRangeException() + }; + } } internal static SKFontMetrics ToFontMetrics(this TextStyle style) { - return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics); + return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs new file mode 100644 index 0000000..4c6d21f --- /dev/null +++ b/QuestPDF/Elements/Dynamic.cs @@ -0,0 +1,104 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Drawing.Exceptions; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class DynamicHost : Element, IStateResettable + { + private DynamicComponentProxy Child { get; } + private object InitialComponentState { get; set; } + + internal TextStyle TextStyle { get; } = new(); + + public DynamicHost(DynamicComponentProxy child) + { + Child = child; + + InitialComponentState = Child.GetState(); + } + + public void ResetState() + { + Child.SetState(InitialComponentState); + } + + internal override SpacePlan Measure(Size availableSpace) + { + var result = GetContent(availableSpace, acceptNewState: false); + var content = result.Content as Element ?? Empty.Instance; + var measurement = content.Measure(availableSpace); + + if (measurement.Type != SpacePlanType.FullRender) + throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page."); + + return result.HasMoreContent + ? SpacePlan.PartialRender(measurement) + : SpacePlan.FullRender(measurement); + } + + internal override void Draw(Size availableSpace) + { + var content = GetContent(availableSpace, acceptNewState: true).Content as Element; + content?.Draw(availableSpace); + } + + private DynamicComponentComposeResult GetContent(Size availableSize, bool acceptNewState) + { + var componentState = Child.GetState(); + + var context = new DynamicContext + { + PageNumber = PageContext.CurrentPage, + PageContext = PageContext, + Canvas = Canvas, + TextStyle = TextStyle, + + AvailableSize = availableSize + }; + + var result = Child.Compose(context); + + if (!acceptNewState) + Child.SetState(componentState); + + return result; + } + } + + public class DynamicContext + { + internal IPageContext PageContext { get; set; } + internal ICanvas Canvas { get; set; } + internal TextStyle TextStyle { get; set; } + + public int PageNumber { get; internal set; } + public Size AvailableSize { get; internal set; } + + public IDynamicElement CreateElement(Action content) + { + var container = new DynamicElement(); + content(container); + + container.ApplyDefaultTextStyle(TextStyle); + container.VisitChildren(x => x?.Initialize(PageContext, Canvas)); + container.VisitChildren(x => (x as IStateResettable)?.ResetState()); + + container.Size = container.Measure(Size.Max); + + return container; + } + } + + public interface IDynamicElement : IElement + { + Size Size { get; } + } + + internal class DynamicElement : ContainerElement, IDynamicElement + { + public Size Size { get; internal set; } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/Table.cs b/QuestPDF/Elements/Table/Table.cs index f508576..4a33c25 100644 --- a/QuestPDF/Elements/Table/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table private int RowsCount { get; set; } private int CurrentRow { get; set; } + // cache that stores all cells + // first index: row number + // inner table: list of all cells that ends at the corresponding row + private TableCell[][] CellsCache { get; set; } + private int MaxRow { get; set; } + internal override void Initialize(IPageContext pageContext, ICanvas canvas) { StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max(); RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max(); Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList(); + BuildCache(); base.Initialize(pageContext, canvas); } - + internal override IEnumerable GetChildren() { return Cells; @@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table Cells.ForEach(x => x.IsRendered = false); CurrentRow = 1; } + + private void BuildCache() + { + if (CellsCache != null) + return; + + if (Cells.Count == 0) + { + MaxRow = 0; + CellsCache = Array.Empty(); + + return; + } + + var groups = Cells + .GroupBy(x => x.Row + x.RowSpan - 1) + .ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray()); + + MaxRow = groups.Max(x => x.Key); + + CellsCache = Enumerable + .Range(0, MaxRow + 1) + .Select(x => groups.ContainsKey(x) ? groups[x] : Array.Empty()) + .ToArray(); + } internal override SpacePlan Measure(Size availableSpace) { @@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table var rowBottomOffsets = new DynamicDictionary(); var commands = new List(); - var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow); + var cellsToTry = Enumerable + .Range(CurrentRow, MaxRow - CurrentRow + 1) + .SelectMany(x => CellsCache[x]); + var currentRow = CurrentRow; var maxRenderingRow = RowsCount; diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 39d493e..26807d3 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -12,7 +12,7 @@ namespace QuestPDF.Elements.Text.Items private const char Space = ' '; public string Text { get; set; } - public TextStyle Style { get; set; } = new TextStyle(); + public TextStyle Style { get; set; } = new(); private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new (); @@ -119,29 +119,53 @@ namespace QuestPDF.Elements.Text.Items // there is no available space to wrap text // if the item is first within the line, perform safe mode and chop the word // otherwise, move the item into the next line - return isFirstElementInLine ? (textLength, textLength + 1) : null; + return isFirstElementInLine ? (textLength, textLength) : null; } public virtual void Draw(TextDrawingRequest request) { var fontMetrics = Style.ToFontMetrics(); + var glyphOffset = GetGlyphOffset(); 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.DrawText(text, Position.Zero, Style); + request.Canvas.DrawText(text, new Position(0, glyphOffset), Style); // draw underline if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue) - DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1); + { + var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset; + DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1); + } // draw stroke if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue) - DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1); - + { + var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1; + strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f; + + DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness); + } + 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.DrawRectangle(new Position(0, offset), new Size(request.TextSize.Width, thickness), Style.Color); + } + + float GetGlyphOffset() + { + var fontSize = Style.Size ?? 12f; + + var offsetFactor = Style.FontPosition switch + { + FontPosition.Normal => 0, + FontPosition.Subscript => 0.1f, + FontPosition.Superscript => -0.35f, + _ => throw new ArgumentOutOfRangeException() + }; + + return fontSize * offsetFactor; } } } diff --git a/QuestPDF/Fluent/DynamicComponentExtensions.cs b/QuestPDF/Fluent/DynamicComponentExtensions.cs new file mode 100644 index 0000000..5fe013b --- /dev/null +++ b/QuestPDF/Fluent/DynamicComponentExtensions.cs @@ -0,0 +1,14 @@ +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class DynamicComponentExtensions + { + public static void Dynamic(this IContainer element, IDynamicComponent dynamicElement) where TState : struct + { + var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement); + element.Element(new DynamicHost(componentProxy)); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index b808eb5..c2d6d81 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -135,7 +135,7 @@ namespace QuestPDF.Fluent private TextPageNumberDescriptor PageNumber(Func pageNumber) { var style = DefaultStyle.Clone(); - var descriptor = new TextPageNumberDescriptor(DefaultStyle); + var descriptor = new TextPageNumberDescriptor(style); AddItemToLastTextBlock(new TextBlockPageNumber { diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs index f30653b..41af136 100644 --- a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -126,7 +126,30 @@ namespace QuestPDF.Fluent { return descriptor.Weight(FontWeight.ExtraBlack); } - + + #endregion + + #region Position + public static T NormalPosition(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Normal); + } + + public static T Subscript(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Subscript); + } + + public static T Superscript(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Position(FontPosition.Superscript); + } + + private static T Position(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor + { + descriptor.TextStyle.FontPosition = fontPosition; + return descriptor; + } #endregion } } \ No newline at end of file diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 2686432..7823908 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -133,7 +133,32 @@ namespace QuestPDF.Fluent { return style.Weight(FontWeight.ExtraBlack); } - + + #endregion + + #region Position + public static TextStyle NormalPosition(this TextStyle style) + { + return style.Position(FontPosition.Normal); + } + + public static TextStyle Subscript(this TextStyle style) + { + return style.Position(FontPosition.Subscript); + } + + public static TextStyle Superscript(this TextStyle style) + { + return style.Position(FontPosition.Superscript); + } + + private static TextStyle Position(this TextStyle style, FontPosition fontPosition) + { + if (style.FontPosition == fontPosition) + return style; + + return style.Mutate(t => t.FontPosition = fontPosition); + } #endregion } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/FontPosition.cs b/QuestPDF/Infrastructure/FontPosition.cs new file mode 100644 index 0000000..8ac8131 --- /dev/null +++ b/QuestPDF/Infrastructure/FontPosition.cs @@ -0,0 +1,9 @@ +namespace QuestPDF.Infrastructure +{ + internal enum FontPosition + { + Normal, + Subscript, + Superscript, + } +} diff --git a/QuestPDF/Infrastructure/IDynamicComponent.cs b/QuestPDF/Infrastructure/IDynamicComponent.cs new file mode 100644 index 0000000..c7fb26c --- /dev/null +++ b/QuestPDF/Infrastructure/IDynamicComponent.cs @@ -0,0 +1,35 @@ +using System; +using QuestPDF.Elements; +using QuestPDF.Helpers; + +namespace QuestPDF.Infrastructure +{ + internal class DynamicComponentProxy + { + internal Action SetState { get; private set; } + internal Func GetState { get; private set; } + internal Func Compose { get; private set; } + + internal static DynamicComponentProxy CreateFrom(IDynamicComponent component) where TState : struct + { + return new DynamicComponentProxy + { + GetState = () => component.State, + SetState = x => component.State = (TState)x, + Compose = component.Compose + }; + } + } + + public class DynamicComponentComposeResult + { + public IElement Content { get; set; } + public bool HasMoreContent { get; set; } + } + + public interface IDynamicComponent where TState : struct + { + TState State { get; set; } + DynamicComponentComposeResult Compose(DynamicContext context); + } +} \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index a0a72df..d734988 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -13,6 +13,7 @@ namespace QuestPDF.Infrastructure internal float? Size { get; set; } internal float? LineHeight { get; set; } internal FontWeight? FontWeight { get; set; } + internal FontPosition? FontPosition { get; set; } internal bool? IsItalic { get; set; } internal bool? HasStrikethrough { get; set; } internal bool? HasUnderline { get; set; } @@ -29,6 +30,7 @@ namespace QuestPDF.Infrastructure Size = 12, LineHeight = 1.2f, FontWeight = Infrastructure.FontWeight.Normal, + FontPosition = Infrastructure.FontPosition.Normal, IsItalic = false, HasStrikethrough = false, HasUnderline = false, @@ -45,7 +47,7 @@ namespace QuestPDF.Infrastructure HasGlobalStyleApplied = true; ApplyParentStyle(globalStyle); - PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color); + PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic); } @@ -57,6 +59,7 @@ namespace QuestPDF.Infrastructure Size ??= parentStyle.Size; LineHeight ??= parentStyle.LineHeight; FontWeight ??= parentStyle.FontWeight; + FontPosition ??= parentStyle.FontPosition; IsItalic ??= parentStyle.IsItalic; HasStrikethrough ??= parentStyle.HasStrikethrough; HasUnderline ??= parentStyle.HasUnderline; @@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure Size = parentStyle.Size ?? Size; LineHeight = parentStyle.LineHeight ?? LineHeight; FontWeight = parentStyle.FontWeight ?? FontWeight; + FontPosition = parentStyle.FontPosition ?? FontPosition; IsItalic = parentStyle.IsItalic ?? IsItalic; HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough; HasUnderline = parentStyle.HasUnderline ?? HasUnderline; diff --git a/QuestPDF/Previewer/PreviewerExtensions.cs b/QuestPDF/Previewer/PreviewerExtensions.cs index b3c401c..17eadbc 100644 --- a/QuestPDF/Previewer/PreviewerExtensions.cs +++ b/QuestPDF/Previewer/PreviewerExtensions.cs @@ -20,7 +20,7 @@ namespace QuestPDF.Previewer { var previewerService = new PreviewerService(port); - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel(); await previewerService.Connect(); diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index 5884003..bce0826 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -24,7 +24,7 @@ namespace QuestPDF.Previewer HttpClient = new() { BaseAddress = new Uri($"http://localhost:{port}/"), - Timeout = TimeSpan.FromMilliseconds(250) + Timeout = TimeSpan.FromSeconds(1) }; } @@ -46,7 +46,7 @@ namespace QuestPDF.Previewer { try { - var result = await HttpClient.GetAsync("/ping"); + using var result = await HttpClient.GetAsync("/ping"); return result.IsSuccessStatusCode; } catch @@ -57,7 +57,7 @@ namespace QuestPDF.Previewer private async Task GetPreviewerVersion() { - var result = await HttpClient.GetAsync("/version"); + using var result = await HttpClient.GetAsync("/version"); return await result.Content.ReadFromJsonAsync(); } @@ -81,6 +81,7 @@ namespace QuestPDF.Previewer Task.Run(async () => { await process.WaitForExitAsync(); + process.Dispose(); OnPreviewerStopped?.Invoke(); }); } @@ -93,17 +94,17 @@ namespace QuestPDF.Previewer private void CheckVersionCompatibility(Version version) { - if (version.Major == 2022 && version.Minor == 4) + if (version.Major == 2022 && version.Minor == 5) return; throw new Exception($"Previewer version is not compatible. Possible solutions: " + $"1) Update the QuestPDF library to newer version. " + - $"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.4'"); + $"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.5'"); } private async Task WaitForConnection() { - var cancellationTokenSource = new CancellationTokenSource(); + using var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10)); var cancellationToken = cancellationTokenSource.Token; @@ -124,7 +125,7 @@ namespace QuestPDF.Previewer public async Task RefreshPreview(ICollection pictures) { - var multipartContent = new MultipartFormDataContent(); + using var multipartContent = new MultipartFormDataContent(); var pages = new List(); @@ -149,7 +150,7 @@ namespace QuestPDF.Previewer multipartContent.Add(JsonContent.Create(command), "command"); - await HttpClient.PostAsync("/update/preview", multipartContent); + using var _ = await HttpClient.PostAsync("/update/preview", multipartContent); foreach (var picture in pictures) picture.Picture.Dispose(); diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 61bd3b6..a4a3d93 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF - 2022.4.1 + 2022.5.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. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 9 diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index a181c0b..8931772 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,6 +1,4 @@ -Release theme: -Introduced the QuestPDF Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates its preview every time you make a code change. You don't need to recompile your code after every small adjustment. Save time and enjoy the design process! (available only for dotnet 6 and beyond) - -Other changes: -- Improved default word-wrapping algorithm to better handle words which do not fit on the available width, -- Introduced new word-wrapping option 'WrapAnywhere' that wraps word at the last possible character instead of moving it into new line. +- Implemented the DynamicComponent element (useful for most advanced cases, e.g. per-page totals), +- Extend text rendering capabilities by adding subscript and superscript effects, +- Improved table rendering performance, +- Previewer tool stability fixes. \ No newline at end of file