Compare commits

...

20 Commits

Author SHA1 Message Date
MarcinZiabek 0ebf2833af Updated release notes 2022-05-06 21:17:16 +02:00
MarcinZiabek 25e6d9fbca Adjusted library versions 2022-05-06 21:09:16 +02:00
Marcin Ziąbek f80149792d Merge pull request #178 from jnyrup/Dispose
Explicit disposal of disposables
2022-05-06 20:59:35 +02:00
Marcin Ziąbek 2beee1d22e Update PreviewerService.cs 2022-05-06 20:59:06 +02:00
MarcinZiabek 1af7c0a144 Adjusted code related to subscript/superscript 2022-05-06 20:55:18 +02:00
Marcin Ziąbek eeca67e5bc Merge pull request #179 from Bebo-Maker/main
Implement subscript/superscript text
2022-05-06 00:09:31 +02:00
MarcinZiabek 4da3ef5dfa Fixed breaking long words 2022-05-05 23:52:17 +02:00
MarcinZiabek f3d4151273 Improved performance of the table rendering algorithm (square to linear complexity) 2022-05-05 23:20:23 +02:00
MarcinZiabek a2629575e8 Dynamic: simplified container composition process 2022-04-27 09:50:17 +02:00
MarcinZiabek c8824021cc Dynamic: generalized dynamic element size measurement 2022-04-27 09:24:44 +02:00
MarcinZiabek e1101d870d Dynamic: improve state management 2022-04-27 06:56:13 +02:00
MarcinZiabek 152304b467 Dynamic: improved TextStyle handling 2022-04-26 01:11:40 +02:00
MarcinZiabek 8a1be90c4f Ported dynamic component implementation prototype 2022-04-26 00:36:50 +02:00
MarcinZiabek bc8a9e5dc5 Fixed mutating text style for page-related text items 2022-04-23 23:13:38 +02:00
Bebo-Maker c63b0658f3 Ignore FontPosition when calculating font metrics. 2022-04-12 19:14:00 +02:00
Bebo-Maker 82cd1d7835 Add example for superscript/subscript. 2022-04-12 18:49:34 +02:00
Bebo-Maker 3ffab6e933 Implement superscript and subscript. 2022-04-12 18:48:28 +02:00
Marcin Ziąbek e4d55e0298 Merge pull request #177 from jnyrup/ArgumentException
Use `paramName` overload of `ArgumentException`
2022-04-11 22:03:40 +02:00
Jonas Nyrup a6f4fd7f49 explicit disposal 2022-04-11 21:36:50 +02:00
Jonas Nyrup 048a4748b2 Use paramName overload of ArgumentException 2022-04-11 21:33:33 +02:00
22 changed files with 658 additions and 41 deletions
+185
View File
@@ -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<OrdersTableState>
{
private ICollection<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(ICollection<OrderItem> items)
{
Items = items;
State = new OrdersTableState
{
ShownItemsCount = 0
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var header = ComposeHeader(context);
var sampleFooter = ComposeFooter(context, Enumerable.Empty<OrderItem>());
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<OrderItem> 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));
});
});
}
}
}
+46
View File
@@ -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}");
});
});
}
}
}
+93 -1
View File
@@ -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, massenergy 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();
});
});
});
+1 -4
View File
@@ -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();
}
}
@@ -63,7 +63,7 @@ namespace QuestPDF.Previewer
private void OpenLink(string path)
{
var openBrowserProcess = new Process
using var openBrowserProcess = new Process
{
StartInfo = new()
{
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.4.1</Version>
<Version>2022.5.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
+4 -1
View File
@@ -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)
+24 -2
View File
@@ -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);
}
}
}
+104
View File
@@ -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<IContainer> 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; }
}
}
+37 -2
View File
@@ -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<Element?> 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<TableCell[]>();
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<TableCell>())
.ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
@@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table
var rowBottomOffsets = new DynamicDictionary<int, float>();
var commands = new List<TableCellRenderingCommand>();
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;
+31 -7
View File
@@ -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;
}
}
}
@@ -0,0 +1,14 @@
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class DynamicComponentExtensions
{
public static void Dynamic<TState>(this IContainer element, IDynamicComponent<TState> dynamicElement) where TState : struct
{
var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
element.Element(new DynamicHost(componentProxy));
}
}
}
+3 -3
View File
@@ -135,7 +135,7 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{
var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(DefaultStyle);
var descriptor = new TextPageNumberDescriptor(style);
AddItemToLastTextBlock(new TextBlockPageNumber
{
@@ -185,7 +185,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor SectionLink(string? text, string sectionName)
{
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -212,7 +212,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Hyperlink(string? text, string url)
{
if (IsNullOrEmpty(url))
throw new ArgumentException(nameof(url));
throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -126,7 +126,30 @@ namespace QuestPDF.Fluent
{
return descriptor.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Normal);
}
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Subscript);
}
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Superscript);
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
return descriptor;
}
#endregion
}
}
+26 -1
View File
@@ -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
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
internal enum FontPosition
{
Normal,
Subscript,
Superscript,
}
}
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
internal class DynamicComponentProxy
{
internal Action<object> SetState { get; private set; }
internal Func<object> GetState { get; private set; }
internal Func<DynamicContext, DynamicComponentComposeResult> Compose { get; private set; }
internal static DynamicComponentProxy CreateFrom<TState>(IDynamicComponent<TState> 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<TState> where TState : struct
{
TState State { get; set; }
DynamicComponentComposeResult Compose(DynamicContext context);
}
}
+5 -1
View File
@@ -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;
+1 -1
View File
@@ -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();
+9 -8
View File
@@ -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<Version> GetPreviewerVersion()
{
var result = await HttpClient.GetAsync("/version");
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
@@ -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<PreviewerPicture> pictures)
{
var multipartContent = new MultipartFormDataContent();
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
@@ -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();
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.4.1</Version>
<Version>2022.5.0</Version>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
+4 -6
View File
@@ -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.