Initial text rendering improvements
This commit is contained in:
Родитель
feab280a27
Коммит
748b8f65f5
|
@ -502,25 +502,49 @@ namespace QuestPDF.Examples
|
|||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(400, 250)
|
||||
.PageSize(300, 175)
|
||||
.FileName()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(25)
|
||||
.Stack(stack =>
|
||||
.Background(Colors.White)
|
||||
.Padding(10)
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
var scales = new[] { 0.75f, 1f, 1.25f, 1.5f };
|
||||
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.8f, 0.9f, 1.1f, 1.2f };
|
||||
|
||||
foreach (var scale in scales)
|
||||
{
|
||||
stack
|
||||
.Item()
|
||||
.Border(1)
|
||||
.Scale(scale)
|
||||
.Padding(10)
|
||||
.Text($"Content with {scale} scale.", TextStyle.Default.Size(20));
|
||||
}
|
||||
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(5)
|
||||
.Text($"Content with {scale} scale.", fontStyle);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
@ -35,18 +42,39 @@ namespace QuestPDF.Examples.Engine
|
|||
Size = new Size(width, height);
|
||||
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);
|
||||
|
||||
Process.Start("explorer", fileNameSchema(0));
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -92,25 +92,7 @@ namespace QuestPDF.Fluent
|
|||
MinHeight = minHeight
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -37,7 +42,17 @@ 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();
|
||||
|
|
Загрузка…
Ссылка в новой задаче