Initial text rendering improvements

This commit is contained in:
Marcin Ziąbek 2021-08-09 22:35:39 +02:00
Родитель feab280a27
Коммит 748b8f65f5
11 изменённых файлов: 410 добавлений и 41 удалений

Просмотреть файл

@ -502,27 +502,51 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.PageSize(400, 250)
.PageSize(300, 175)
.FileName()
.Render(container =>
{
container
.Padding(25)
.Background(Colors.White)
.Padding(10)
.Decoration(decoration =>
{
var headerFontStyle = TextStyle
.Default
.Size(20)
.Color(Colors.Blue.Darken2)
.SemiBold();
decoration
.Header()
.PaddingBottom(10)
.Text("Example: scale component", headerFontStyle);
decoration
.Content()
.Stack(stack =>
{
var scales = new[] { 0.75f, 1f, 1.25f, 1.5f };
var scales = new[] { 0.8f, 0.9f, 1.1f, 1.2f };
foreach (var scale in scales)
{
var fontColor = scale <= 1f
? Colors.Red.Lighten4
: Colors.Green.Lighten4;
var fontStyle = TextStyle.Default.Size(16);
stack
.Item()
.Border(1)
.Background(fontColor)
.Scale(scale)
.Padding(10)
.Text($"Content with {scale} scale.", TextStyle.Default.Size(20));
.Padding(5)
.Text($"Content with {scale} scale.", fontStyle);
}
});
});
});
}
[Test]

Просмотреть файл

@ -9,10 +9,17 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Examples.Engine
{
public enum RenderingTestResult
{
Pdf,
Images
}
public class RenderingTest
{
private string FileNamePrefix = "test";
private Size Size { get; set; }
private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
private RenderingTest()
{
@ -36,17 +43,38 @@ namespace QuestPDF.Examples.Engine
return this;
}
public RenderingTest ProducePdf()
{
ResultType = RenderingTestResult.Pdf;
return this;
}
public RenderingTest ProduceImages()
{
ResultType = RenderingTestResult.Images;
return this;
}
public void Render(Action<IContainer> content)
{
var container = new Container();
content(container);
Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
var document = new SimpleDocument(container, Size);
document.GenerateImages(fileNameSchema);
if (ResultType == RenderingTestResult.Images)
{
Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
document.GenerateImages(fileNameSchema);
Process.Start("explorer", fileNameSchema(0));
}
if (ResultType == RenderingTestResult.Pdf)
{
var fileName = $"{FileNamePrefix}.pdf";
document.GeneratePdf(fileName);
Process.Start("explorer", fileName);
}
}
}
}

Просмотреть файл

@ -0,0 +1,33 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class TextExamples
{
[Test]
public void TextElements()
{
RenderingTest
.Create()
.PageSize(600, 400)
.FileName()
.ProducePdf()
.Render(container =>
{
container.Padding(20).Text(text =>
{
text.Span("Let's start with something bold...", TextStyle.Default.SemiBold().Size(18));
text.Span("And BIG...", TextStyle.Default.Size(28).Color(Colors.DeepOrange.Darken2).BackgroundColor(Colors.Yellow.Lighten3).Underlined());
text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16));
//text.Element().ExternalLink("https://www.questpdf.com/").Width(200).Height(50).Text("Visit questpdf.com", TextStyle.Default.Underlined().Color(Colors.Blue.Darken2));
text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16).Stroked());
text.Span("And now it's time for some colors 12345 678 90293 03490 83290.", TextStyle.Default.Size(20).Color(Colors.Green.Medium));
});
});
}
}
}

Просмотреть файл

@ -0,0 +1,15 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.SpacePlan
{
internal class TextRender : FullRender
{
public float Ascent { get; set; }
public float Descent { get; set; }
public TextRender(float width, float height) : base(width, height)
{
}
}
}

Просмотреть файл

@ -18,16 +18,15 @@ namespace QuestPDF.Elements
{
container
.Background(Colors.Grey.Lighten2)
.Padding(5)
.AlignMiddle()
.AlignCenter()
.Padding(5)
.MaxHeight(32)
.Element(x =>
{
if (string.IsNullOrWhiteSpace(Text))
x.Image(ImageData, ImageScaling.FitArea);
x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea);
else
x.Text(Text, TextStyle.Default.Size(14).SemiBold());
x.Text(Text, TextStyle.Default.Size(14));
});
}
}

Просмотреть файл

@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class TextBlock : Element, IStateResettable
{
public List<Element> Children { get; set; } = new List<Element>();
public Queue<Element> ChildrenQueue { get; set; } = new Queue<Element>();
public void ResetState()
{
ChildrenQueue = new Queue<Element>(Children);
}
internal override void HandleVisitor(Action<Element?> visit)
{
Children.ForEach(x => x?.HandleVisitor(visit));
base.HandleVisitor(visit);
}
internal override ISpacePlan Measure(Size availableSpace)
{
return new FullRender(availableSpace);
if (!ChildrenQueue.Any())
return new FullRender(Size.Zero);
if (Children.Count < 50)
return new FullRender(Size.Zero);
var items = SelectItemsForCurrentLine(availableSpace);
if (items == null)
return new Wrap();
var totalWidth = items.Sum(x => x.Measurement.Width);
var totalHeight = items.Max(x => x.Measurement.Height);
return new PartialRender(totalWidth, totalHeight);
return new FullRender(Size.Zero);
return CreateParent(availableSpace).Measure(availableSpace);
}
internal override void Draw(Size availableSpace)
{
while (true)
{
if (!ChildrenQueue.Any())
return;
var items = SelectItemsForCurrentLine(availableSpace);
if (items == null)
return;
var totalWidth = items.Sum(x => x.Measurement.Width);
var totalHeight = items.Max(x => x.Measurement.Height);
var spaceBetween = (availableSpace.Width - totalWidth) / (items.Count - 1);
var offset = items
.Select(x => x.Measurement)
.Cast<TextRender>()
.Where(x => x != null)
.Select(x => x.Ascent)
.Select(Math.Abs)
.Max();
Canvas.Translate(new Position(0, offset));
foreach (var item in items)
{
item.Element.Draw(availableSpace);
Canvas.Translate(new Position(item.Measurement.Width + spaceBetween, 0));
}
Canvas.Translate(new Position(-availableSpace.Width - spaceBetween, totalHeight - offset));
items.ForEach(x => ChildrenQueue.Dequeue());
}
}
Container CreateParent(Size availableSpace)
{
var children = Children
.Select(x => new
{
Element = x,
Measurement = x.Measure(Size.Max) as Size
})
.Select(x => new GridElement()
{
Child = x.Element,
Columns = (int)x.Measurement.Width + 1
})
.ToList();
var grid = new Grid()
{
Alignment = HorizontalAlignment.Left,
ColumnsCount = (int)availableSpace.Width,
Children = children
};
var container = new Container();
grid.Compose(container);
container.HandleVisitor(x => x.Initialize(PageContext, Canvas));
return container;
}
private List<MeasuredElement>? SelectItemsForCurrentLine(Size availableSpace)
{
var totalWidth = 0f;
var items = ChildrenQueue
.Select(x => new MeasuredElement
{
Element = x,
Measurement = x.Measure(Size.Max) as FullRender
})
.TakeWhile(x =>
{
if (x.Measurement == null)
return false;
if (totalWidth + x.Measurement.Width > availableSpace.Width)
return false;
totalWidth += x.Measurement.Width;
return true;
})
.ToList();
if (items.Any(x => x.Measurement == null))
return null;
if (items.Max(x => x.Measurement.Height) > availableSpace.Height)
return null;
return items;
}
private class MeasuredElement
{
public Element Element { get; set; }
public FullRender? Measurement { get; set; }
}
}
}

Просмотреть файл

@ -0,0 +1,58 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class TextItem : Element
{
public string Value { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
internal override ISpacePlan Measure(Size availableSpace)
{
var paint = Style.ToPaint();
var metrics = paint.FontMetrics;
var width = paint.MeasureText(Value);
var height = Math.Abs(metrics.Descent) + Math.Abs(metrics.Ascent);
if (availableSpace.Width < width || availableSpace.Height < height)
return new Wrap();
return new TextRender(width, height)
{
Descent = metrics.Descent,
Ascent = metrics.Ascent
};
}
internal override void Draw(Size availableSpace)
{
var paint = Style.ToPaint();
var metrics = paint.FontMetrics;
var size = Measure(availableSpace) as Size;
if (size == null)
return;
Canvas.DrawRectangle(new Position(0, metrics.Ascent), new Size(size.Width, size.Height), Style.BackgroundColor);
Canvas.DrawText(Value, Position.Zero, Style);
// draw underline
if (Style.IsUnderlined && metrics.UnderlinePosition.HasValue)
DrawLine(metrics.UnderlinePosition.Value, metrics.UnderlineThickness.Value);
// draw stroke
if (Style.IsStroked && metrics.StrikeoutPosition.HasValue)
DrawLine(metrics.StrikeoutPosition.Value, metrics.StrikeoutThickness.Value);
void DrawLine(float offset, float thickness)
{
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(size.Width, thickness), Style.Color);
}
}
}
}

Просмотреть файл

@ -93,24 +93,6 @@ namespace QuestPDF.Fluent
});
}
public static void Text(this IContainer element, object text, TextStyle? style = null)
{
text ??= string.Empty;
style ??= TextStyle.Default;
if (element is Alignment alignment)
{
style = style.Clone();
style.Alignment = alignment.Horizontal;
}
element.Element(new Text
{
Value = text.ToString(),
Style = style
});
}
public static void PageBreak(this IContainer element)
{
element.Element(new PageBreak());

Просмотреть файл

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class TextDescriptor
{
internal ICollection<Element> Elements = new List<Element>();
internal TextDescriptor()
{
}
public void Span(string text, TextStyle? style = null)
{
text.Split(' ')
.Select(x => $"{x} ")
.Select(x => new TextItem
{
Value = x,
Style = style ?? TextStyle.Default
})
.ToList()
.ForEach(Elements.Add);
}
public IContainer Element()
{
var container = new Container();
Elements.Add(container);
return container;
}
}
public static class TextExtensions
{
public static void Text(this IContainer element, Action<TextDescriptor> content)
{
var descriptor = new TextDescriptor();
content?.Invoke(descriptor);
element.Element(new TextBlock()
{
Children = descriptor.Elements.ToList()
});
}
public static void Text(this IContainer element, object text, TextStyle? style = null)
{
element.Text(x => x.Span(text.ToString(), style));
}
}
}

Просмотреть файл

@ -18,6 +18,11 @@ namespace QuestPDF.Fluent
return style.Mutate(x => x.Color = value);
}
public static TextStyle BackgroundColor(this TextStyle style, string value)
{
return style.Mutate(x => x.BackgroundColor = value);
}
public static TextStyle FontType(this TextStyle style, string value)
{
return style.Mutate(x => x.FontType = value);
@ -38,6 +43,16 @@ namespace QuestPDF.Fluent
return style.Mutate(x => x.IsItalic = value);
}
public static TextStyle Stroked(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.IsStroked = value);
}
public static TextStyle Underlined(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.IsUnderlined = value);
}
#region Alignmnet
public static TextStyle Alignment(this TextStyle style, HorizontalAlignment value)

Просмотреть файл

@ -5,18 +5,21 @@ namespace QuestPDF.Infrastructure
public class TextStyle
{
internal string Color { get; set; } = Colors.Black;
internal string BackgroundColor { get; set; } = Colors.Transparent;
internal string FontType { get; set; } = "Calibri";
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;
internal bool IsStroked { get; set; } = false;
internal bool IsUnderlined { get; set; } = false;
public static TextStyle Default => new TextStyle();
public override string ToString()
{
return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}";
return $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}|{IsStroked}|{IsUnderlined}";
}
internal TextStyle Clone() => (TextStyle)MemberwiseClone();