This commit is contained in:
Marcin Ziąbek 2021-08-27 12:40:06 +02:00
Родитель a08a7b85c9
Коммит bb8e590fc6
14 изменённых файлов: 349 добавлений и 300 удалений

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

@ -0,0 +1,16 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
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; }
}
}

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

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements.Text.Items;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextLine
{
public ICollection<TextLineElement> Elements { get; set; }
public float TextHeight => Elements.Max(x => 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;
public float Width => Elements.Sum(x => x.Measurement.Width);
}
}

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

@ -0,0 +1,10 @@
using QuestPDF.Elements.Text.Calculation;
namespace QuestPDF.Elements.Text.Items
{
internal class TextLineElement
{
public ITextBlockElement Element { get; set; }
public TextMeasurementResult Measurement { get; set; }
}
}

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

@ -0,0 +1,13 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
}
}

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

@ -0,0 +1,21 @@
using System;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementResult
{
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; }
public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex;
}
}

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

@ -0,0 +1,10 @@
using QuestPDF.Elements.Text.Calculation;
namespace QuestPDF.Elements.Text.Items
{
internal interface ITextBlockElement
{
TextMeasurementResult? Measure(TextMeasurementRequest request);
void Draw(TextDrawingRequest request);
}
}

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

@ -0,0 +1,48 @@
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockElement : ITextBlockElement
{
public Element Element { get; set; } = Empty.Instance;
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
if (measurement is Wrap || measurement is PartialRender)
return null;
var elementSize = measurement as Size;
return new TextMeasurementResult
{
Width = elementSize.Width,
Ascent = -elementSize.Height,
Descent = 0,
LineHeight = 1,
StartIndex = 0,
EndIndex = 0,
TotalIndex = 0
};
}
public void Draw(TextDrawingRequest request)
{
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
request.Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
}
}
}

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

@ -0,0 +1,35 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockExternalLink : ITextBlockElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string Url { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextItem GetItem()
{
return new TextItem
{
Style = Style,
Text = Text
};
}
}
}

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

@ -0,0 +1,35 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockInternalLink : ITextBlockElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string LocationName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextItem GetItem()
{
return new TextItem
{
Style = Style,
Text = Text
};
}
}
}

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

@ -0,0 +1,36 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockPageNumber : ITextBlockElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string SlotName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem(request.PageContext).MeasureWithoutCache(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()
};
}
}
}

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

@ -0,0 +1,93 @@
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextItem : ITextBlockElement
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
return MeasureCache[cacheKey];
}
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
// start breaking text from requested position
var text = Text.Substring(request.StartIndex);
var breakingIndex = (int)paint.BreakText(text, request.AvailableWidth);
if (breakingIndex <= 0)
return null;
// break text only on spaces
if (breakingIndex < text.Length)
{
breakingIndex = text.Substring(0, breakingIndex).LastIndexOf(" ");
if (breakingIndex <= 0)
return null;
breakingIndex += 1;
}
text = text.Substring(0, breakingIndex);
// measure final text
var width = paint.MeasureText(text);
return new TextMeasurementResult
{
Width = width,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight,
StartIndex = request.StartIndex,
EndIndex = request.StartIndex + breakingIndex,
TotalIndex = Text.Length
};
}
public void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
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);
}
}
}
}

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

@ -1,40 +1,23 @@
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text
{
internal class TextLineElement
{
public ITextElement Element { get; set; }
public TextMeasurementResult Measurement { get; set; }
}
internal class TextLine
{
public ICollection<TextLineElement> Elements { get; set; }
public float TextHeight => Elements.Max(x => 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;
public float Width => Elements.Sum(x => x.Measurement.Width);
}
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextElement> Children { get; set; } = new List<ITextElement>();
public List<ITextBlockElement> Children { get; set; } = new List<ITextBlockElement>();
public Queue<ITextElement> RenderingQueue { get; set; }
public Queue<ITextBlockElement> RenderingQueue { get; set; }
public int CurrentElementIndex { get; set; }
public void ResetState()
{
RenderingQueue = new Queue<ITextElement>(Children);
RenderingQueue = new Queue<ITextBlockElement>(Children);
CurrentElementIndex = 0;
}
@ -137,7 +120,7 @@ namespace QuestPDF.Elements.Text
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
{
var queue = new Queue<ITextElement>(RenderingQueue);
var queue = new Queue<ITextBlockElement>(RenderingQueue);
var currentItemIndex = CurrentElementIndex;
var currentHeight = 0f;

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

@ -1,271 +0,0 @@
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Drawing.SpacePlan;
using QuestPDF.Infrastructure;
using Size = QuestPDF.Infrastructure.Size;
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; }
}
internal class TextMeasurementResult
{
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; }
public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex;
}
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 : ITextElement
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new TextStyle();
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
return MeasureCache[cacheKey];
}
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
// start breaking text from requested position
var text = Text.Substring(request.StartIndex);
var breakingIndex = (int)paint.BreakText(text, request.AvailableWidth);
if (breakingIndex <= 0)
return null;
// break text only on spaces
if (breakingIndex < text.Length)
{
breakingIndex = text.Substring(0, breakingIndex).LastIndexOf(" ");
if (breakingIndex <= 0)
return null;
breakingIndex += 1;
}
text = text.Substring(0, breakingIndex);
// measure final text
var width = paint.MeasureText(text);
return new TextMeasurementResult
{
Width = width,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight,
StartIndex = request.StartIndex,
EndIndex = request.StartIndex + breakingIndex,
TotalIndex = Text.Length
};
}
public void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
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).MeasureWithoutCache(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()
};
}
}
internal class InternalLinkTextItem : ITextElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string LocationName { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextItem GetItem()
{
return new TextItem
{
Style = Style,
Text = Text
};
}
}
internal class ExternalLinkTextItem : ITextElement
{
public TextStyle Style { get; set; } = new TextStyle();
public string Text { get; set; }
public string Url { get; set; }
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return GetItem().MeasureWithoutCache(request);
}
public void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
GetItem().Draw(request);
}
private TextItem GetItem()
{
return new TextItem
{
Style = Style,
Text = Text
};
}
}
internal class ElementTextItem : ITextElement
{
public Element Element { get; set; } = Empty.Instance;
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
if (measurement is Wrap || measurement is PartialRender)
return null;
var elementSize = measurement as Size;
return new TextMeasurementResult
{
Width = elementSize.Width,
Ascent = -elementSize.Height,
Descent = 0,
LineHeight = 1,
StartIndex = 0,
EndIndex = 0,
TotalIndex = 0
};
}
public void Draw(TextDrawingRequest request)
{
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas));
request.Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
}
}
}

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
@ -39,7 +40,7 @@ namespace QuestPDF.Fluent
Spacing = value;
}
private void AddItemToLastTextBlock(ITextElement element)
private void AddItemToLastTextBlock(ITextBlockElement element)
{
if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock());
@ -69,7 +70,7 @@ namespace QuestPDF.Fluent
.Skip(1)
.Select(x => new TextBlock
{
Children = new List<ITextElement> { x }
Children = new List<ITextBlockElement> { x }
})
.ToList()
.ForEach(TextBlocks.Add);
@ -84,7 +85,7 @@ namespace QuestPDF.Fluent
{
style ??= DefaultStyle;
AddItemToLastTextBlock(new PageNumberTextItem()
AddItemToLastTextBlock(new TextBlockPageNumber()
{
Style = style,
SlotName = slotName
@ -110,7 +111,7 @@ namespace QuestPDF.Fluent
{
style ??= DefaultStyle;
AddItemToLastTextBlock(new InternalLinkTextItem
AddItemToLastTextBlock(new TextBlockInternalLink
{
Style = style,
Text = text,
@ -122,7 +123,7 @@ namespace QuestPDF.Fluent
{
style ??= DefaultStyle;
AddItemToLastTextBlock(new ExternalLinkTextItem
AddItemToLastTextBlock(new TextBlockExternalLink
{
Style = style,
Text = text,
@ -134,7 +135,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
AddItemToLastTextBlock(new ElementTextItem
AddItemToLastTextBlock(new TextBlockElement
{
Element = container
});