Implemented text rendering benchmark

This commit is contained in:
Marcin Ziąbek
2021-08-26 22:25:33 +02:00
parent 6023b4de34
commit f17860f519
11 changed files with 9296 additions and 116 deletions
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Examples
{
[TestFixture]
public class BarcodeExamples
{
[Test]
public void Barcode()
{
RenderingTest
.Create()
.PageSize(300, 300)
.FileName()
.Render(container =>
{
container
.Background("#FFF")
.Padding(25)
.Stack(stack =>
{
stack.Item().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Barcode Example");
stack.Item().Border(1).Padding(5).AlignCenter().Text("*123456789*", TextStyle.Default.FontType("CarolinaBar-Demo-25E2").Size(20));
});
});
}
}
}
+16 -4
View File
@@ -19,6 +19,7 @@ namespace QuestPDF.Examples.Engine
{
private string FileNamePrefix = "test";
private Size Size { get; set; }
private bool ShowResult { get; set; }
private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
private RenderingTest()
@@ -59,26 +60,37 @@ namespace QuestPDF.Examples.Engine
ResultType = RenderingTestResult.Images;
return this;
}
public RenderingTest ShowResults()
{
ShowResult = true;
return this;
}
public void Render(Action<IContainer> content)
{
var container = new Container();
content(container);
var document = new SimpleDocument(container, Size);
var maxPages = ResultType == RenderingTestResult.Pdf ? 1000 : 10;
var document = new SimpleDocument(container, Size, maxPages);
if (ResultType == RenderingTestResult.Images)
{
Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
document.GenerateImages(fileNameSchema);
Process.Start("explorer", fileNameSchema(0));
if (ShowResult)
Process.Start("explorer", fileNameSchema(0));
}
if (ResultType == RenderingTestResult.Pdf)
{
var fileName = $"{FileNamePrefix}.pdf";
document.GeneratePdf(fileName);
Process.Start("explorer", fileName);
if (ShowResult)
Process.Start("explorer", fileName);
}
}
}
+4 -2
View File
@@ -12,11 +12,13 @@ namespace QuestPDF.Examples.Engine
private IContainer Container { get; }
private Size Size { get; }
private int MaxPages { get; }
public SimpleDocument(IContainer container, Size size)
public SimpleDocument(IContainer container, Size size, int maxPages)
{
Container = container;
Size = size;
MaxPages = maxPages;
}
public DocumentMetadata GetMetadata()
@@ -24,7 +26,7 @@ namespace QuestPDF.Examples.Engine
return new DocumentMetadata()
{
RasterDpi = PageSizes.PointsPerInch * ImageScalingFactor,
DocumentLayoutExceptionThreshold = 10
DocumentLayoutExceptionThreshold = MaxPages
};
}
+1 -1
View File
@@ -17,7 +17,7 @@
</ItemGroup>
<ItemGroup>
<None Update="LibreBarcode39-Regular.ttf">
<None Update="quo-vadis.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
+177
View File
@@ -0,0 +1,177 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class TextBenchmark
{
[Test]
public void Benchmark()
{
var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
var normalStyle = TextStyle.Default.Size(14);
var chapters = GetChapters().ToList();
var results = PerformTest(16).ToList();
Console.WriteLine($"Min: {results.Min():F}");
Console.WriteLine($"Max: {results.Max():F}");
Console.WriteLine($"Avg: {results.Average():F}");
IEnumerable<(string title, string content)> GetChapters()
{
var book = File.ReadAllLines("quo-vadis.txt");
var chapterPointers = book
.Select((line, index) => new
{
LineNumber = index,
Text = line
})
.Where(x => x.Text.Length < 50 && x.Text.Contains("Rozdział") || x.Text.Contains("-----"))
.Select(x => x.LineNumber)
.ToList();
foreach (var index in Enumerable.Range(0, chapterPointers.Count - 1))
{
var chapter = chapterPointers[index];
var title = book[chapter];
var lineFrom = chapterPointers[index];
var lineTo = chapterPointers[index + 1] - 1;
var lines = book.Skip(lineFrom + 1).Take(lineTo - lineFrom);
var content = string.Join(Environment.NewLine, lines);
yield return (title, content);
}
}
void GenerateDocument()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.FileName()
.ProducePdf()
.Render(ComposePage);
}
IEnumerable<float> PerformTest(int attempts)
{
foreach (var i in Enumerable.Range(0, attempts))
{
var timer = new Stopwatch();
timer.Start();
GenerateDocument();
timer.Stop();
Console.WriteLine($"Attempt {i}: {timer.ElapsedMilliseconds:F}");
yield return timer.ElapsedMilliseconds;
}
}
void ComposePage(IContainer container)
{
container
.Padding(50)
.Decoration(decoration =>
{
decoration
.Content()
.Stack(stack =>
{
stack.Item().Element(Title);
stack.Item().PageBreak();
stack.Item().Element(TableOfContents);
stack.Item().PageBreak();
Chapters(stack);
});
decoration.Footer().Element(Footer);
});
}
void Title(IContainer container)
{
container
.Extend()
.PaddingBottom(200)
.AlignBottom()
.Stack(stack =>
{
stack.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2));
stack.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2));
});
}
void TableOfContents(IContainer container)
{
container.Stack(stack =>
{
stack.Item().Text("Table of contents", subtitleStyle);
stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
foreach (var chapter in chapters)
{
stack.Item().InternalLink(chapter.title).Row(row =>
{
row.RelativeColumn().Text(chapter.title);
row.ConstantColumn(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.title, normalStyle));
});
}
});
}
void Chapters(StackDescriptor stack)
{
foreach (var chapter in chapters)
{
stack.Item().Element(container => Chapter(container, chapter.title, chapter.content));
}
}
void Chapter(IContainer container, string title, string content)
{
container.Stack(stack =>
{
stack.Item().Location(title).Text(title, subtitleStyle);
stack.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal();
stack.Item().Text(text =>
{
text.ParagraphSpacing(10);
text.Span(content, normalStyle);
});
stack.Item().PageBreak();
});
}
void Footer(IContainer container)
{
container
.AlignCenter()
.Text(text =>
{
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
}
}
}
}
+5 -1
View File
@@ -1,4 +1,6 @@
using System.Linq;
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
@@ -30,9 +32,11 @@ namespace QuestPDF.Examples
text.Span("Then something bigger. ", TextStyle.Default.Size(28).Color(Colors.DeepOrange.Darken2).BackgroundColor(Colors.Yellow.Lighten3).Underlined());
text.Span("And tiny teeny-tiny. ", TextStyle.Default.Size(6));
text.Span("Stroked text also works fine. ", TextStyle.Default.Size(14).Stroked().BackgroundColor(Colors.Grey.Lighten4));
//text.NewLine();
text.Span("Is it time for lorem ipsum? ", TextStyle.Default.Size(12).Underlined().BackgroundColor(Colors.Grey.Lighten3));
text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(12));
//text.NewLine();
text.Span("And now some colors: ", TextStyle.Default.Size(16).Color(Colors.Green.Medium));
foreach (var i in Enumerable.Range(1, 100))
+34
View File
@@ -0,0 +1,34 @@
# Text optimization
## Initial state
Attempts:
```angular2html
Attempt 0: 18389,00
Attempt 1: 18627,00
Attempt 2: 19745,00
Attempt 3: 19690,00
Attempt 4: 19032,00
Attempt 5: 17773,00
Attempt 6: 17570,00
Attempt 7: 17691,00
Attempt 8: 17642,00
Attempt 9: 17945,00
Attempt 10: 19876,00
Attempt 11: 19731,00
Attempt 12: 19158,00
Attempt 13: 18004,00
Attempt 14: 17734,00
Attempt 15: 19352,00
```
Results:
```
Min: 17570,00
Max: 19876,00
Avg: 18622,44
```
File diff suppressed because one or more lines are too long
@@ -1,17 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
namespace QuestPDF.Elements.Text
{
internal class TextLineElement
{
public TextItem Element { get; set; }
public ITextElement Element { get; set; }
public TextMeasurementResult Measurement { get; set; }
}
@@ -20,7 +17,7 @@ namespace QuestPDF.Elements
public ICollection<TextLineElement> Elements { get; set; }
public float TextHeight => Elements.Max(x => x.Measurement.Height);
public float LineHeight => Elements.Max(x => x.Element.Style.LineHeight * x.Measurement.Height);
public float LineHeight => Elements.Max(x => x.Measurement.LineHeight * x.Measurement.Height);
public float Ascent => Elements.Min(x => x.Measurement.Ascent) - (LineHeight - TextHeight) / 2;
public float Descent => Elements.Max(x => x.Measurement.Descent) + (LineHeight - TextHeight) / 2;
@@ -31,23 +28,17 @@ namespace QuestPDF.Elements
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<TextItem> Children { get; set; } = new List<TextItem>();
public List<ITextElement> Children { get; set; } = new List<ITextElement>();
public Queue<TextItem> RenderingQueue { get; set; }
public Queue<ITextElement> RenderingQueue { get; set; }
public int CurrentElementIndex { get; set; }
public void ResetState()
{
RenderingQueue = new Queue<TextItem>(Children);
RenderingQueue = new Queue<ITextElement>(Children);
CurrentElementIndex = 0;
}
internal override void HandleVisitor(Action<Element?> visit)
{
Children.ForEach(x => x?.HandleVisitor(visit));
base.HandleVisitor(visit);
}
internal override ISpacePlan Measure(Size availableSpace)
{
if (!RenderingQueue.Any())
@@ -103,6 +94,9 @@ namespace QuestPDF.Elements
{
var textDrawingRequest = new TextDrawingRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = item.Measurement.StartIndex,
EndIndex = item.Measurement.EndIndex,
@@ -144,7 +138,7 @@ namespace QuestPDF.Elements
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
{
var queue = new Queue<TextItem>(RenderingQueue);
var queue = new Queue<ITextElement>(RenderingQueue);
var currentItemIndex = CurrentElementIndex;
var currentHeight = 0f;
@@ -177,11 +171,14 @@ namespace QuestPDF.Elements
var measurementRequest = new TextMeasurementRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth
};
var measurementResponse = currentElement.MeasureText(measurementRequest);
var measurementResponse = currentElement.Measure(measurementRequest);
if (measurementResponse == null)
break;
@@ -193,7 +190,7 @@ namespace QuestPDF.Elements
});
currentWidth += measurementResponse.Width;
currentItemIndex = measurementResponse.EndIndex + 1;
currentItemIndex = measurementResponse.EndIndex;
if (!measurementResponse.IsLast)
break;
@@ -1,16 +1,16 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using QuestPDF.Drawing;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Infrastructure;
using SkiaSharp;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements
namespace QuestPDF.Elements.Text
{
internal class TextMeasurementRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
}
@@ -19,10 +19,12 @@ namespace QuestPDF.Elements
{
public float Width { get; set; }
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
public float Ascent { get; set; }
public float Descent { get; set; }
public float LineHeight { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
@@ -32,86 +34,30 @@ namespace QuestPDF.Elements
public bool IsLast => EndIndex == TotalIndex;
}
public class TextDrawingRequest
internal class TextDrawingRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public float TotalAscent { get; set; }
public Size TextSize { get; set; }
}
internal interface ITextElement
{
TextMeasurementResult? Measure(TextMeasurementRequest request);
void Draw(TextDrawingRequest request);
}
internal class TextItem : Element, IStateResettable
internal class TextItem : ITextElement
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
internal int PointerIndex { get; set; }
public void ResetState()
{
PointerIndex = 0;
}
internal override ISpacePlan Measure(Size availableSpace)
{
return new FullRender(Size.Zero);
// if (VirtualPointer >= Text.Length)
// return new FullRender(Size.Zero);
//
// var paint = Style.ToPaint();
// var metrics = paint.FontMetrics;
//
// var length = (int)paint.BreakText(Text, availableSpace.Width);
// length = VirtualPointer + Text.Substring(VirtualPointer, length).LastIndexOf(" ");
//
// var textFragment = Text.Substring(VirtualPointer, length);
//
// var width = paint.MeasureText(textFragment);
// var height = Math.Abs(metrics.Descent) + Math.Abs(metrics.Ascent);
//
// if (availableSpace.Width < width || availableSpace.Height < height)
// return new Wrap();
//
// VirtualPointer += length;
//
// return new TextRender(width, height)
// {
// Descent = metrics.Descent,
// Ascent = metrics.Ascent
// };
}
internal override void Draw(Size availableSpace)
{
}
internal void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToPaint().FontMetrics;
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
Canvas.DrawText(text, Position.Zero, Style);
// draw underline
if (Style.IsUnderlined && fontMetrics.UnderlinePosition.HasValue)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
// draw stroke
if (Style.IsStroked && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value);
void DrawLine(float offset, float thickness)
{
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
internal TextMeasurementResult? MeasureText(TextMeasurementRequest request)
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var paint = Style.ToPaint();
@@ -129,6 +75,8 @@ namespace QuestPDF.Elements
if (breakingIndex <= 0)
return null;
breakingIndex += 1;
}
text = text.Substring(0, breakingIndex);
@@ -143,10 +91,66 @@ namespace QuestPDF.Elements
Ascent = paint.FontMetrics.Ascent,
Descent = paint.FontMetrics.Descent,
LineHeight = Style.LineHeight,
StartIndex = request.StartIndex,
EndIndex = request.StartIndex + breakingIndex,
TotalIndex = Text.Length
};
}
public void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToPaint().FontMetrics;
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);
// draw underline
if (Style.IsUnderlined && fontMetrics.UnderlinePosition.HasValue)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value);
// draw stroke
if (Style.IsStroked && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value);
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
internal class PageNumberTextItem : ITextElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string SlotName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem(request.PageContext).Measure(request);
}
public void Draw(TextDrawingRequest request)
{
GetItem(request.PageContext).Draw(request);
}
private TextItem GetItem(IPageContext context)
{
var pageNumberPlaceholder = 123;
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
? context.GetLocationPage(SlotName)
: pageNumberPlaceholder;
return new TextItem
{
Style = Style,
Text = pageNumber.ToString()
};
}
}
}
+85 -16
View File
@@ -2,40 +2,110 @@
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class TextDescriptor
{
private TextBlock TextBlock { get; }
internal TextDescriptor(TextBlock textBlock)
{
TextBlock = textBlock;
}
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
private HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
private float Spacing { get; set; } = 0f;
public void AlignLeft()
{
TextBlock.Alignment = HorizontalAlignment.Left;
Alignment = HorizontalAlignment.Left;
}
public void AlignCenter()
{
TextBlock.Alignment = HorizontalAlignment.Center;
Alignment = HorizontalAlignment.Center;
}
public void AlignRight()
{
TextBlock.Alignment = HorizontalAlignment.Right;
Alignment = HorizontalAlignment.Right;
}
public void ParagraphSpacing(float value)
{
Spacing = value;
}
public void Span(string text, TextStyle? style = null)
{
TextBlock.Children.Add(new TextItem
style ??= TextStyle.Default;
if (string.IsNullOrWhiteSpace(text))
return;
var items = text
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => new TextItem
{
Text = x,
Style = style
})
.ToList();
if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock());
TextBlocks.First().Children.Add(items.First());
items
.Skip(1)
.Select(x => new TextBlock
{
Children = new List<ITextElement> { x }
})
.ToList()
.ForEach(TextBlocks.Add);
}
public void NewLine()
{
Span(Environment.NewLine);
}
private void PageNumber(string slotName, TextStyle? style = null)
{
style ??= TextStyle.Default;
if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock());
TextBlocks.First().Children.Add(new PageNumberTextItem()
{
Text = text,
Style = style ?? TextStyle.Default
Style = style,
SlotName = slotName
});
}
public void CurrentPageNumber(TextStyle? style = null)
{
PageNumber(PageContext.CurrentPageSlot, style);
}
public void TotalPages(TextStyle? style = null)
{
PageNumber(PageContext.TotalPagesSlot, style);
}
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
{
PageNumber(locationName, style);
}
internal void Compose(IContainer container)
{
container.Stack(stack =>
{
stack.Spacing(Spacing);
foreach (var textBlock in TextBlocks)
stack.Item().Element(textBlock);
});
}
}
@@ -49,10 +119,9 @@ namespace QuestPDF.Fluent
if (element is Alignment alignment)
textBlock.Alignment = alignment.Horizontal;
var descriptor = new TextDescriptor(textBlock);
var descriptor = new TextDescriptor();
content?.Invoke(descriptor);
element.Element(textBlock);
descriptor.Compose(element);
}
public static void Text(this IContainer element, object text, TextStyle? style = null)