Compare commits
10 Commits
2022.2.5
...
text-shaping
| Author | SHA1 | Date | |
|---|---|---|---|
| 662a55e21c | |||
| 7d825d123b | |||
| 054e7a5f20 | |||
| a92cc25fb1 | |||
| 8db283d559 | |||
| 8a64abaac2 | |||
| 68df390d0e | |||
| d73c694452 | |||
| 4479a49ad0 | |||
| 78eef45ce2 |
@@ -13,11 +13,11 @@ namespace QuestPDF.Examples
|
||||
[Test]
|
||||
public void Example()
|
||||
{
|
||||
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
|
||||
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(400, 100)
|
||||
.PageSize(400, 200)
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
@@ -25,7 +25,7 @@ namespace QuestPDF.Examples
|
||||
.Background(Colors.White)
|
||||
.AlignCenter()
|
||||
.AlignMiddle()
|
||||
.Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
|
||||
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Diagnostics;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using ShimSkiaSharp;
|
||||
using SKTypeface = SkiaSharp.SKTypeface;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class MinimalApiExamples
|
||||
{
|
||||
[Test]
|
||||
public void MinimalApi()
|
||||
{
|
||||
Document
|
||||
.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A5);
|
||||
page.Margin(1.5f, Unit.Centimetre);
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!", TextStyle.Default.SemiBold().Size(20));
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(10);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
});
|
||||
});
|
||||
})
|
||||
.GeneratePdf("hello.pdf");
|
||||
|
||||
Process.Start("explorer.exe", "hello.pdf");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,7 +236,7 @@ namespace QuestPDF.Examples
|
||||
.Padding(10)
|
||||
.Text(text =>
|
||||
{
|
||||
text.DefaultTextStyle(TextStyle.Default);
|
||||
text.DefaultTextStyle(TextStyle.Default.BackgroundColor(Colors.Grey.Lighten3));
|
||||
text.AlignLeft();
|
||||
text.ParagraphSpacing(10);
|
||||
|
||||
@@ -251,7 +251,7 @@ namespace QuestPDF.Examples
|
||||
|
||||
text.EmptyLine();
|
||||
|
||||
foreach (var i in Enumerable.Range(1, 100))
|
||||
foreach (var i in Enumerable.Range(1, 1000))
|
||||
{
|
||||
text.Line($"{i}: {Placeholders.Paragraph()}");
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class TextShapingTests
|
||||
{
|
||||
[Test]
|
||||
public void ShapeText()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
using var backgroundPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.LightGray
|
||||
};
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(550, 250)
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
//var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
|
||||
//var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور";
|
||||
|
||||
var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
|
||||
var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
|
||||
var text = arabic;
|
||||
var metrics = textPaint.FontMetrics;
|
||||
|
||||
container
|
||||
.Padding(25)
|
||||
.Canvas((canvas, space) =>
|
||||
{
|
||||
canvas.Translate(0, 20);
|
||||
|
||||
var width = MeasureText(text, textPaint);
|
||||
var widthReal = textPaint.MeasureText(text);
|
||||
canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
|
||||
|
||||
canvas.DrawShapedText(text, 0, 0, textPaint);
|
||||
|
||||
canvas.Translate(0, 40);
|
||||
canvas.DrawText(text, 0, 0, textPaint);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
float MeasureText(string text, SKPaint paint)
|
||||
{
|
||||
var font = paint.ToFont();
|
||||
using var shaper = new SKShaper(paint.Typeface);
|
||||
var result = shaper.Shape(text + " ", paint);
|
||||
return Math.Max(result.Points.First().X, result.Points.Last().X);
|
||||
|
||||
var glyphCount = result.Codepoints.Length;
|
||||
var glyphIds = new ushort[glyphCount];
|
||||
var glyphWidths = new float[glyphCount];
|
||||
var glyphBounds = new SKRect[glyphCount];
|
||||
|
||||
font.GetGlyphs(result.Codepoints.Select(x => (int)x).ToArray(), glyphIds);
|
||||
font.GetGlyphWidths(glyphIds, glyphWidths, glyphBounds);
|
||||
|
||||
return glyphWidths.Sum();
|
||||
|
||||
return result.Points.Max(p => p.X);
|
||||
|
||||
using var skTextBlobBuilder = new SKTextBlobBuilder();
|
||||
|
||||
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(font, result.Codepoints.Length);
|
||||
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
|
||||
var positionSpan = positionedRunBuffer.GetPositionSpan();
|
||||
|
||||
for (int index = 0; index < result.Codepoints.Length; ++index)
|
||||
{
|
||||
glyphSpan[index] = (ushort) result.Codepoints[index];
|
||||
positionSpan[index] = result.Points[index];
|
||||
}
|
||||
|
||||
using var text1 = skTextBlobBuilder.Build();
|
||||
return text1.Bounds.Width;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShapeSingle()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
var text = string.Join(" ", Enumerable.Range(0, 10_000).Select(x => Placeholders.Sentence()));
|
||||
|
||||
using var shaper = new SKShaper(textPaint.Typeface);
|
||||
var result = shaper.Shape(text + " ", textPaint);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShapeMany()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
var text = string.Join(" ", Enumerable.Range(0, 1_000).Select(x => Placeholders.Sentence()));
|
||||
|
||||
foreach (var part in Regex.Split(text, "( )|(\\w+)"))
|
||||
{
|
||||
using var shaper = new SKShaper(textPaint.Typeface);
|
||||
var result = shaper.Shape(part + " ", textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShapeMany2()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
using var shaper = new SKShaper(textPaint.Typeface);
|
||||
|
||||
foreach (var i in Enumerable.Range(0, 10_000))
|
||||
{
|
||||
var text = Placeholders.Paragraph();
|
||||
|
||||
textPaint.MeasureText(text);
|
||||
var result = shaper.Shape(text + " ", textPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -24,5 +25,32 @@ namespace QuestPDF.ReportSample
|
||||
Latitude = Helpers.Random.NextDouble() * 180f - 90f
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<int, string> RomanNumeralCache = new ConcurrentDictionary<int, string>();
|
||||
|
||||
public static string FormatAsRomanNumeral(this int number)
|
||||
{
|
||||
if (number < 0 || number > 3999)
|
||||
throw new ArgumentOutOfRangeException("Number should be in range from 1 to 3999");
|
||||
|
||||
return RomanNumeralCache.GetOrAdd(number, x =>
|
||||
{
|
||||
if (x >= 1000) return "M" + FormatAsRomanNumeral(x - 1000);
|
||||
if (x >= 900) return "CM" + FormatAsRomanNumeral(x - 900);
|
||||
if (x >= 500) return "D" + FormatAsRomanNumeral(x - 500);
|
||||
if (x >= 400) return "CD" + FormatAsRomanNumeral(x - 400);
|
||||
if (x >= 100) return "C" + FormatAsRomanNumeral(x - 100);
|
||||
if (x >= 90) return "XC" + FormatAsRomanNumeral(x - 90);
|
||||
if (x >= 50) return "L" + FormatAsRomanNumeral(x - 50);
|
||||
if (x >= 40) return "XL" + FormatAsRomanNumeral(x - 40);
|
||||
if (x >= 10) return "X" + FormatAsRomanNumeral(x - 10);
|
||||
if (x >= 9) return "IX" + FormatAsRomanNumeral(x - 9);
|
||||
if (x >= 5) return "V" + FormatAsRomanNumeral(x - 5);
|
||||
if (x >= 4) return "IV" + FormatAsRomanNumeral(x - 4);
|
||||
if (x >= 1) return "I" + FormatAsRomanNumeral(x - 1);
|
||||
|
||||
return string.Empty;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,9 +39,9 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
|
||||
page.Footer().AlignCenter().Text(text =>
|
||||
{
|
||||
text.CurrentPageNumber();
|
||||
text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
text.Span(" / ");
|
||||
text.TotalPages();
|
||||
text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.ReportSample.Layouts
|
||||
@@ -43,7 +45,18 @@ namespace QuestPDF.ReportSample.Layouts
|
||||
{
|
||||
row.ConstantItem(25).Text($"{number}.");
|
||||
row.RelativeItem().Text(locationName);
|
||||
row.ConstantItem(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName));
|
||||
row.ConstantItem(150).AlignRight().Text(text =>
|
||||
{
|
||||
text.BeginPageNumberOfSection(locationName);
|
||||
text.Span(" - ");
|
||||
text.EndPageNumberOfSection(locationName);
|
||||
|
||||
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
|
||||
|
||||
text.Span(" (", lengthStyle);
|
||||
text.TotalPagesWithinSection(locationName, lengthStyle, x => x == 1 ? "1 page long" : $"{x} pages long");
|
||||
text.Span(")", lengthStyle);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
||||
public class ExternalLinkTests
|
||||
{
|
||||
[Test]
|
||||
public void Measure() => SimpleContainerTests.Measure<ExternalLink>();
|
||||
public void Measure() => SimpleContainerTests.Measure<Hyperlink>();
|
||||
|
||||
// TODO: consider tests for the Draw method
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using SkiaSharp;
|
||||
using static SkiaSharp.SKFontStyleSlant;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FontStyleSetTests
|
||||
{
|
||||
private void ExpectComparisonOrder(SKFontStyle target, SKFontStyle[] styles)
|
||||
{
|
||||
for (var i = 0; i < styles.Length - 1; i++)
|
||||
{
|
||||
var currentStyle = styles[i];
|
||||
var nextStyle = styles[i + 1];
|
||||
|
||||
FontStyleSet.IsBetterMatch(target, currentStyle, nextStyle).Should().BeTrue();
|
||||
FontStyleSet.IsBetterMatch(target, nextStyle, currentStyle).Should().BeFalse();
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_CondensedWidth()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 5, Upright),
|
||||
new SKFontStyle(500, 4, Upright),
|
||||
new SKFontStyle(500, 3, Upright),
|
||||
new SKFontStyle(500, 6, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ExpandedWidth()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 6, Upright),
|
||||
new SKFontStyle(500, 7, Upright),
|
||||
new SKFontStyle(500, 8, Upright),
|
||||
new SKFontStyle(500, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 6, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ItalicSlant()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 5, Italic),
|
||||
new SKFontStyle(500, 5, Oblique),
|
||||
new SKFontStyle(500, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 5, Italic), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ObliqueSlant()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 5, Oblique),
|
||||
new SKFontStyle(500, 5, Italic),
|
||||
new SKFontStyle(500, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 5, Oblique), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_UprightSlant()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 5, Upright),
|
||||
new SKFontStyle(500, 5, Oblique),
|
||||
new SKFontStyle(500, 5, Italic)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_ThinWeight()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(300, 5, Upright),
|
||||
new SKFontStyle(200, 5, Upright),
|
||||
new SKFontStyle(100, 5, Upright),
|
||||
new SKFontStyle(400, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(300, 5, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_RegularWeight()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(500, 5, Upright),
|
||||
new SKFontStyle(300, 5, Upright),
|
||||
new SKFontStyle(100, 5, Upright),
|
||||
new SKFontStyle(600, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(400, 5, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_IsBetterMatch_BoldWeight()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(600, 5, Upright),
|
||||
new SKFontStyle(700, 5, Upright),
|
||||
new SKFontStyle(800, 5, Upright),
|
||||
new SKFontStyle(500, 5, Upright)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(600, 5, Upright), styles);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontStyleSet_RespectsPriority()
|
||||
{
|
||||
var styles = new[]
|
||||
{
|
||||
new SKFontStyle(600, 5, Italic),
|
||||
new SKFontStyle(600, 6, Upright),
|
||||
new SKFontStyle(500, 6, Italic)
|
||||
};
|
||||
|
||||
ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
||||
public class InternalLinkTests
|
||||
{
|
||||
[Test]
|
||||
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
|
||||
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
|
||||
|
||||
// TODO: consider tests for the Draw method
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests
|
||||
public class InternalLocationTests
|
||||
{
|
||||
[Test]
|
||||
public void Measure() => SimpleContainerTests.Measure<InternalLink>();
|
||||
public void Measure() => SimpleContainerTests.Measure<SectionLink>();
|
||||
|
||||
// TODO: consider tests for the Draw method
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
|
||||
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
|
||||
public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
|
||||
public void DrawShapedText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
|
||||
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
|
||||
|
||||
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
|
||||
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
|
||||
public void DrawLocation(string locationName) => throw new NotImplementedException();
|
||||
|
||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
||||
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
|
||||
public void DrawSection(string sectionName) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,11 @@ namespace QuestPDF.UnitTests.TestEngine
|
||||
|
||||
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
|
||||
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
|
||||
public void DrawShapedText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
|
||||
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
|
||||
|
||||
public void DrawExternalLink(string url, Size size) => throw new NotImplementedException();
|
||||
public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException();
|
||||
public void DrawLocation(string locationName) => throw new NotImplementedException();
|
||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
||||
public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException();
|
||||
public void DrawSection(string sectionName) => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
public static class FontManager
|
||||
{
|
||||
private static ConcurrentDictionary<string, SKTypeface> Typefaces = new ConcurrentDictionary<string, SKTypeface>();
|
||||
private static ConcurrentDictionary<string, SKFontMetrics> FontMetrics = new ConcurrentDictionary<string, SKFontMetrics>();
|
||||
private static ConcurrentDictionary<string, SKPaint> Paints = new ConcurrentDictionary<string, SKPaint>();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new ConcurrentDictionary<string, SKPaint>();
|
||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
||||
private static ConcurrentDictionary<object, SKPaint> Paints = new();
|
||||
private static ConcurrentDictionary<object, SKFont> Fonts = new();
|
||||
private static ConcurrentDictionary<object, SKShaper> Shapers = new();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
|
||||
|
||||
private static void RegisterFontType(SKData fontData, string? customName = null)
|
||||
{
|
||||
foreach (var index in Enumerable.Range(0, 256))
|
||||
{
|
||||
var typeface = SKTypeface.FromData(fontData, index);
|
||||
|
||||
if (typeface == null)
|
||||
break;
|
||||
|
||||
var typefaceName = customName ?? typeface.FamilyName;
|
||||
|
||||
var fontStyleSet = StyleSets.GetOrAdd(typefaceName, _ => new FontStyleSet());
|
||||
fontStyleSet.Add(typeface);
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFontType(Stream stream) overload.")]
|
||||
public static void RegisterFontType(string fontName, Stream stream)
|
||||
{
|
||||
Typefaces.TryAdd(fontName, SKTypeface.FromStream(stream));
|
||||
using var fontData = SKData.Create(stream);
|
||||
RegisterFontType(fontData);
|
||||
RegisterFontType(fontData, customName: fontName);
|
||||
}
|
||||
|
||||
|
||||
public static void RegisterFontType(Stream stream)
|
||||
{
|
||||
using var fontData = SKData.Create(stream);
|
||||
RegisterFontType(fontData);
|
||||
}
|
||||
|
||||
internal static SKPaint ColorToPaint(this string color)
|
||||
{
|
||||
return ColorPaint.GetOrAdd(color, Convert);
|
||||
@@ -30,37 +59,57 @@ namespace QuestPDF.Drawing
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal static SKPaint ToPaint(this TextStyle style)
|
||||
{
|
||||
return Paints.GetOrAdd(style.Key, key => Convert(style));
|
||||
|
||||
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
|
||||
|
||||
static SKPaint Convert(TextStyle style)
|
||||
{
|
||||
return new SKPaint
|
||||
{
|
||||
Color = SKColor.Parse(style.Color),
|
||||
Typeface = GetTypeface(style),
|
||||
TextSize = (style.Size ?? 12),
|
||||
TextEncoding = SKTextEncoding.Utf32
|
||||
TextSize = style.Size ?? 12
|
||||
};
|
||||
}
|
||||
|
||||
static SKTypeface GetTypeface(TextStyle style)
|
||||
{
|
||||
if (Typefaces.TryGetValue(style.FontType, out var result))
|
||||
return result;
|
||||
|
||||
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
|
||||
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
|
||||
|
||||
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
|
||||
|
||||
if (StyleSets.TryGetValue(style.FontType, out var fontStyleSet))
|
||||
return fontStyleSet.Match(fontStyle);
|
||||
|
||||
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontType, fontStyle);
|
||||
|
||||
return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant)
|
||||
?? throw new ArgumentException($"The typeface {style.FontType} could not be found.");
|
||||
if (fontFromDefaultSource != null)
|
||||
return fontFromDefaultSource;
|
||||
|
||||
throw new ArgumentException(
|
||||
$"The typeface '{style.FontType}' could not be found. " +
|
||||
$"Please consider the following options: " +
|
||||
$"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.");
|
||||
}
|
||||
}
|
||||
|
||||
internal static SKFont ToFont(this TextStyle style)
|
||||
{
|
||||
return Fonts.GetOrAdd(style.FontMetricsKey, _ => style.ToPaint().Typeface.ToFont());
|
||||
}
|
||||
|
||||
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
|
||||
{
|
||||
return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics);
|
||||
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
|
||||
}
|
||||
|
||||
internal static SKShaper ToShaper(this TextStyle style)
|
||||
{
|
||||
return Shapers.GetOrAdd(style.FontMetricsKey, _ => new SKShaper(style.ToFont().Typeface));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class FontStyleSet
|
||||
{
|
||||
private ConcurrentDictionary<SKFontStyle, SKTypeface> Styles { get; } = new();
|
||||
|
||||
public void Add(SKTypeface typeface)
|
||||
{
|
||||
var style = typeface.FontStyle;
|
||||
Styles.AddOrUpdate(style, _ => typeface, (_, _) => typeface);
|
||||
}
|
||||
|
||||
public SKTypeface? Match(SKFontStyle target)
|
||||
{
|
||||
SKFontStyle? bestStyle = null;
|
||||
SKTypeface? bestTypeface = null;
|
||||
|
||||
foreach (var entry in Styles)
|
||||
{
|
||||
if (IsBetterMatch(target, entry.Key, bestStyle))
|
||||
{
|
||||
bestStyle = entry.Key;
|
||||
bestTypeface = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return bestTypeface;
|
||||
}
|
||||
|
||||
private static Dictionary<SKFontStyleSlant, List<SKFontStyleSlant>> SlantFallbacks = new()
|
||||
{
|
||||
{ SKFontStyleSlant.Italic, new() { SKFontStyleSlant.Italic, SKFontStyleSlant.Oblique, SKFontStyleSlant.Upright } },
|
||||
{ SKFontStyleSlant.Oblique, new() { SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic, SKFontStyleSlant.Upright } },
|
||||
{ SKFontStyleSlant.Upright, new() { SKFontStyleSlant.Upright, SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic } },
|
||||
};
|
||||
|
||||
// Checks whether style a is a better match for the target then style b. Uses the CSS font style matching algorithm
|
||||
internal static bool IsBetterMatch(SKFontStyle? target, SKFontStyle? a, SKFontStyle? b)
|
||||
{
|
||||
// A font is better than no font
|
||||
if (b == null)
|
||||
return true;
|
||||
|
||||
if (a == null)
|
||||
return false;
|
||||
|
||||
// First check font width
|
||||
// For normal and condensed widths prefer smaller widths
|
||||
// For expanded widths prefer larger widths
|
||||
if (target.Width <= (int)SKFontStyleWidth.Normal)
|
||||
{
|
||||
if (a.Width <= target.Width && b.Width > target.Width)
|
||||
return true;
|
||||
|
||||
if (a.Width > target.Width && b.Width <= target.Width)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (a.Width >= target.Width && b.Width < target.Width)
|
||||
return true;
|
||||
|
||||
if (a.Width < target.Width && b.Width >= target.Width)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer closest match
|
||||
var widthDifferenceA = Math.Abs(a.Width - target.Width);
|
||||
var widthDifferenceB = Math.Abs(b.Width - target.Width);
|
||||
|
||||
if (widthDifferenceA < widthDifferenceB)
|
||||
return true;
|
||||
|
||||
if (widthDifferenceB < widthDifferenceA)
|
||||
return false;
|
||||
|
||||
// Prefer closest slant based on provided fallback list
|
||||
var slantFallback = SlantFallbacks[target.Slant];
|
||||
var slantIndexA = slantFallback.IndexOf(a.Slant);
|
||||
var slantIndexB = slantFallback.IndexOf(b.Slant);
|
||||
|
||||
if (slantIndexA < slantIndexB)
|
||||
return true;
|
||||
|
||||
if (slantIndexB < slantIndexA)
|
||||
return false;
|
||||
|
||||
// Check weight last
|
||||
// For thin (<400) weights, prefer thinner weights
|
||||
// For regular (400-500) weights, prefer other regular weights, then use rule for thin or bold
|
||||
// For bold (>500) weights, prefer thicker weights
|
||||
// Behavior for values other than multiples of 100 is not given in the specification
|
||||
|
||||
if (target.Weight >= 400 && target.Weight <= 500)
|
||||
{
|
||||
if ((a.Weight >= 400 && a.Weight <= 500) && !(b.Weight >= 400 && b.Weight <= 500))
|
||||
return true;
|
||||
|
||||
if (!(a.Weight >= 400 && a.Weight <= 500) && (b.Weight >= 400 && b.Weight <= 500))
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.Weight < 450)
|
||||
{
|
||||
if (a.Weight <= target.Weight && b.Weight > target.Weight)
|
||||
return true;
|
||||
|
||||
if (a.Weight > target.Weight && b.Weight <= target.Weight)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (a.Weight >= target.Weight && b.Weight < target.Weight)
|
||||
return true;
|
||||
|
||||
if (a.Weight < target.Weight && b.Weight >= target.Weight)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prefer closest weight
|
||||
var weightDifferenceA = Math.Abs(a.Weight - target.Weight);
|
||||
var weightDifferenceB = Math.Abs(b.Weight - target.Weight);
|
||||
|
||||
return weightDifferenceA < weightDifferenceB;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ namespace QuestPDF.Drawing
|
||||
public void DrawText(string text, Position position, TextStyle style)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void DrawShapedText(string text, Position position, TextStyle style)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void DrawImage(SKImage image, Position position, Size size)
|
||||
@@ -51,17 +56,17 @@ namespace QuestPDF.Drawing
|
||||
|
||||
}
|
||||
|
||||
public void DrawExternalLink(string url, Size size)
|
||||
public void DrawHyperlink(string url, Size size)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void DrawLocationLink(string locationName, Size size)
|
||||
public void DrawSectionLink(string sectionName, Size size)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void DrawLocation(string locationName)
|
||||
public void DrawSection(string sectionName)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
@@ -32,24 +33,29 @@ namespace QuestPDF.Drawing
|
||||
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
|
||||
}
|
||||
|
||||
public void DrawShapedText(string text, Position vector, TextStyle style)
|
||||
{
|
||||
Canvas.DrawShapedText(text, vector.X, vector.Y, style.ToPaint());
|
||||
}
|
||||
|
||||
public void DrawImage(SKImage image, Position vector, Size size)
|
||||
{
|
||||
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
|
||||
}
|
||||
|
||||
public void DrawExternalLink(string url, Size size)
|
||||
public void DrawHyperlink(string url, Size size)
|
||||
{
|
||||
Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url);
|
||||
}
|
||||
|
||||
public void DrawLocationLink(string locationName, Size size)
|
||||
public void DrawSectionLink(string sectionName, Size size)
|
||||
{
|
||||
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName);
|
||||
Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName);
|
||||
}
|
||||
|
||||
public void DrawLocation(string locationName)
|
||||
public void DrawSection(string sectionName)
|
||||
{
|
||||
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName);
|
||||
Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName);
|
||||
}
|
||||
|
||||
public void Rotate(float angle)
|
||||
|
||||
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class ExternalLink : ContainerElement
|
||||
internal class Hyperlink : ContainerElement
|
||||
{
|
||||
public string Url { get; set; } = "https://www.questpdf.com";
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
|
||||
if (targetSize.Type == SpacePlanType.Wrap)
|
||||
return;
|
||||
|
||||
Canvas.DrawExternalLink(Url, targetSize);
|
||||
Canvas.DrawHyperlink(Url, targetSize);
|
||||
base.Draw(availableSpace);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class InternalLocation : ContainerElement, IStateResettable
|
||||
internal class Section : ContainerElement, IStateResettable
|
||||
{
|
||||
public string LocationName { get; set; }
|
||||
private bool IsRendered { get; set; }
|
||||
@@ -16,12 +16,11 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
if (!IsRendered)
|
||||
{
|
||||
PageContext.SetLocationPage(LocationName);
|
||||
|
||||
Canvas.DrawLocation(LocationName);
|
||||
Canvas.DrawSection(LocationName);
|
||||
IsRendered = true;
|
||||
}
|
||||
|
||||
PageContext.SetSectionPage(LocationName);
|
||||
base.Draw(availableSpace);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class InternalLink : ContainerElement
|
||||
internal class SectionLink : ContainerElement
|
||||
{
|
||||
public string LocationName { get; set; }
|
||||
public string SectionName { get; set; }
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
|
||||
if (targetSize.Type == SpacePlanType.Wrap)
|
||||
return;
|
||||
|
||||
Canvas.DrawLocationLink(LocationName, targetSize);
|
||||
Canvas.DrawSectionLink(SectionName, targetSize);
|
||||
base.Draw(availableSpace);
|
||||
}
|
||||
}
|
||||
+1
-8
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace QuestPDF.Elements.Text.Calculation
|
||||
{
|
||||
internal class TextMeasurementResult
|
||||
internal class TextBlockSize
|
||||
{
|
||||
public float Width { get; set; }
|
||||
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
|
||||
@@ -11,12 +11,5 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
public float Descent { get; set; }
|
||||
|
||||
public float LineHeight { get; set; }
|
||||
|
||||
public int StartIndex { get; set; }
|
||||
public int EndIndex { get; set; }
|
||||
public int NextIndex { get; set; }
|
||||
public int TotalIndex { get; set; }
|
||||
|
||||
public bool IsLast => EndIndex == TotalIndex;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,8 @@
|
||||
|
||||
namespace QuestPDF.Elements.Text.Calculation
|
||||
{
|
||||
internal class TextDrawingRequest
|
||||
internal struct 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; }
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
internal class TextLineElement
|
||||
{
|
||||
public ITextBlockItem Item { get; set; }
|
||||
public TextMeasurementResult Measurement { get; set; }
|
||||
public TextBlockSize Measurement { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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; }
|
||||
public bool IsFirstLineElement { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal interface ITextBlockItem
|
||||
{
|
||||
TextMeasurementResult? Measure(TextMeasurementRequest request);
|
||||
ICanvas Canvas { get; set; }
|
||||
IPageContext PageContext { get; set; }
|
||||
|
||||
TextBlockSize? Measure();
|
||||
void Draw(TextDrawingRequest request);
|
||||
}
|
||||
}
|
||||
@@ -7,41 +7,40 @@ namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockElement : ITextBlockItem
|
||||
{
|
||||
public ICanvas Canvas { get; set; }
|
||||
public IPageContext PageContext { get; set; }
|
||||
|
||||
public Element Element { get; set; } = Empty.Instance;
|
||||
|
||||
public TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
public TextBlockSize? Measure()
|
||||
{
|
||||
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
|
||||
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
|
||||
|
||||
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
|
||||
var measurement = Element.Measure(Size.Max);
|
||||
|
||||
if (measurement.Type != SpacePlanType.FullRender)
|
||||
return null;
|
||||
|
||||
return new TextMeasurementResult
|
||||
return new TextBlockSize
|
||||
{
|
||||
Width = measurement.Width,
|
||||
|
||||
Ascent = -measurement.Height,
|
||||
Descent = 0,
|
||||
|
||||
LineHeight = 1,
|
||||
|
||||
StartIndex = 0,
|
||||
EndIndex = 0,
|
||||
TotalIndex = 0
|
||||
LineHeight = 1
|
||||
};
|
||||
}
|
||||
|
||||
public void Draw(TextDrawingRequest request)
|
||||
{
|
||||
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
|
||||
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
|
||||
|
||||
request.Canvas.Translate(new Position(0, request.TotalAscent));
|
||||
Canvas.Translate(new Position(0, request.TotalAscent));
|
||||
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
|
||||
request.Canvas.Translate(new Position(0, -request.TotalAscent));
|
||||
Canvas.Translate(new Position(0, -request.TotalAscent));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockExternalLink : TextBlockSpan
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
{
|
||||
return MeasureWithoutCache(request);
|
||||
}
|
||||
|
||||
public override 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));
|
||||
|
||||
base.Draw(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockHyperlink : TextBlockSpan
|
||||
{
|
||||
public string Url { get; set; }
|
||||
|
||||
public override void Draw(TextDrawingRequest request)
|
||||
{
|
||||
Canvas.Translate(new Position(0, request.TotalAscent));
|
||||
Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
|
||||
Canvas.Translate(new Position(0, -request.TotalAscent));
|
||||
|
||||
base.Draw(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockInternalLink : TextBlockSpan
|
||||
{
|
||||
public string LocationName { get; set; }
|
||||
|
||||
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
{
|
||||
return MeasureWithoutCache(request);
|
||||
}
|
||||
|
||||
public override 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));
|
||||
|
||||
base.Draw(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,29 @@
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using System;
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockPageNumber : TextBlockSpan
|
||||
{
|
||||
public string SlotName { get; set; }
|
||||
public const string PageNumberPlaceholder = "123";
|
||||
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
|
||||
|
||||
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
public override TextBlockSize? Measure()
|
||||
{
|
||||
SetPageNumber(request.PageContext);
|
||||
return MeasureWithoutCache(request);
|
||||
SetPageNumber();
|
||||
return base.Measure();
|
||||
}
|
||||
|
||||
public override void Draw(TextDrawingRequest request)
|
||||
{
|
||||
SetPageNumber(request.PageContext);
|
||||
SetPageNumber();
|
||||
base.Draw(request);
|
||||
}
|
||||
|
||||
private void SetPageNumber(IPageContext context)
|
||||
private void SetPageNumber()
|
||||
{
|
||||
var pageNumberPlaceholder = 123;
|
||||
|
||||
var pageNumber = context.GetRegisteredLocations().Contains(SlotName)
|
||||
? context.GetLocationPage(SlotName)
|
||||
: pageNumberPlaceholder;
|
||||
|
||||
Text = pageNumber.ToString();
|
||||
Text = Source(PageContext) ?? PageNumberPlaceholder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockSectionLink : TextBlockSpan
|
||||
{
|
||||
public string SectionName { get; set; }
|
||||
|
||||
public override void Draw(TextDrawingRequest request)
|
||||
{
|
||||
Canvas.Translate(new Position(0, request.TotalAscent));
|
||||
Canvas.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height));
|
||||
Canvas.Translate(new Position(0, -request.TotalAscent));
|
||||
|
||||
base.Draw(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,118 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp.HarfBuzz;
|
||||
using Size = QuestPDF.Infrastructure.Size;
|
||||
|
||||
namespace QuestPDF.Elements.Text.Items
|
||||
{
|
||||
internal class TextBlockSpan : ITextBlockItem
|
||||
{
|
||||
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 virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||
{
|
||||
var cacheKey = (request.StartIndex, request.AvailableWidth);
|
||||
|
||||
if (!MeasureCache.ContainsKey(cacheKey))
|
||||
MeasureCache[cacheKey] = MeasureWithoutCache(request);
|
||||
|
||||
return MeasureCache[cacheKey];
|
||||
}
|
||||
public ICanvas Canvas { get; set; }
|
||||
public IPageContext PageContext { get; set; }
|
||||
|
||||
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
||||
public string Text { get; set; }
|
||||
public TextStyle Style { get; set; }
|
||||
|
||||
protected TextBlockSize? Size { get; set; }
|
||||
protected bool RequiresShaping { get; set; }
|
||||
|
||||
public virtual TextBlockSize? Measure()
|
||||
{
|
||||
Size ??= Text == " " ? GetSizeForSpace() : GetSizeForWord();
|
||||
return Size;
|
||||
}
|
||||
|
||||
private TextBlockSize GetSizeForWord()
|
||||
{
|
||||
const char space = ' ';
|
||||
|
||||
var paint = Style.ToPaint();
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
|
||||
var startIndex = request.StartIndex;
|
||||
var shaper = Style.ToShaper();
|
||||
|
||||
if (request.IsFirstLineElement)
|
||||
{
|
||||
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
|
||||
startIndex++;
|
||||
}
|
||||
|
||||
if (Text.Length == 0)
|
||||
{
|
||||
return new TextMeasurementResult
|
||||
{
|
||||
Width = 0,
|
||||
|
||||
LineHeight = Style.LineHeight ?? 1,
|
||||
Ascent = fontMetrics.Ascent,
|
||||
Descent = fontMetrics.Descent
|
||||
};
|
||||
}
|
||||
// shaper returns positions of all glyphs,
|
||||
// by adding a space, it is possible to capture width of the last original character
|
||||
var result = shaper.Shape(Text + " ", paint);
|
||||
|
||||
// start breaking text from requested position
|
||||
var text = Text.Substring(startIndex);
|
||||
// when text is left-to-right: last value corresponds to text width
|
||||
// when text is right-to-left: glyphs are in the reverse order, first value represents text width
|
||||
var width = Math.Max(result.Points.First().X, result.Points.Last().X);
|
||||
|
||||
RequiresShaping = result.Points.Length != Text.Length + 1;
|
||||
|
||||
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
|
||||
|
||||
if (textLength <= 0)
|
||||
return null;
|
||||
|
||||
if (textLength < text.Length && text[textLength] == space)
|
||||
textLength++;
|
||||
|
||||
// break text only on spaces
|
||||
if (textLength < text.Length)
|
||||
{
|
||||
var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
|
||||
|
||||
if (lastSpaceIndex <= 0)
|
||||
{
|
||||
if (!request.IsFirstLineElement)
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
textLength = lastSpaceIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
text = text.Substring(0, textLength);
|
||||
|
||||
var endIndex = startIndex + textLength;
|
||||
var nextIndex = endIndex;
|
||||
|
||||
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
|
||||
nextIndex++;
|
||||
|
||||
// measure final text
|
||||
var width = paint.MeasureText(text);
|
||||
|
||||
return new TextMeasurementResult
|
||||
return new TextBlockSize
|
||||
{
|
||||
Width = width,
|
||||
|
||||
Ascent = fontMetrics.Ascent,
|
||||
Descent = fontMetrics.Descent,
|
||||
|
||||
LineHeight = Style.LineHeight ?? 1,
|
||||
|
||||
StartIndex = startIndex,
|
||||
EndIndex = endIndex,
|
||||
NextIndex = nextIndex,
|
||||
TotalIndex = Text.Length
|
||||
LineHeight = Style.LineHeight ?? 1
|
||||
};
|
||||
}
|
||||
|
||||
private TextBlockSize GetSizeForSpace()
|
||||
{
|
||||
var paint = Style.ToPaint();
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
|
||||
return new TextBlockSize
|
||||
{
|
||||
Width = paint.MeasureText(" "),
|
||||
|
||||
Ascent = fontMetrics.Ascent,
|
||||
Descent = fontMetrics.Descent,
|
||||
|
||||
LineHeight = Style.LineHeight ?? 1
|
||||
};
|
||||
}
|
||||
|
||||
public virtual 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);
|
||||
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Text))
|
||||
{
|
||||
if (RequiresShaping)
|
||||
Canvas.DrawShapedText(Text, Position.Zero, Style);
|
||||
else
|
||||
Canvas.DrawText(Text, Position.Zero, Style);
|
||||
}
|
||||
|
||||
// draw underline
|
||||
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
|
||||
@@ -124,7 +94,7 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
void DrawLine(float offset, float thickness)
|
||||
{
|
||||
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
|
||||
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Elements.Text.Items;
|
||||
@@ -11,108 +12,145 @@ namespace QuestPDF.Elements.Text
|
||||
internal class TextBlock : Element, IStateResettable
|
||||
{
|
||||
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
|
||||
public List<ITextBlockItem> Items { get; set; } = new();
|
||||
|
||||
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
|
||||
|
||||
private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
||||
private int CurrentElementIndex { get; set; }
|
||||
|
||||
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
|
||||
{
|
||||
Items = SplitElementsBySpace().ToList();
|
||||
|
||||
Items.ForEach(x =>
|
||||
{
|
||||
x.PageContext = pageContext;
|
||||
x.Canvas = canvas;
|
||||
});
|
||||
|
||||
base.Initialize(pageContext, canvas);
|
||||
}
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
RenderingQueue = new Queue<ITextBlockItem>(Items);
|
||||
CurrentElementIndex = 0;
|
||||
}
|
||||
|
||||
private IEnumerable<ITextBlockItem> SplitElementsBySpace()
|
||||
{
|
||||
foreach (var item in Items)
|
||||
{
|
||||
if (item is TextBlockSpan span)
|
||||
{
|
||||
span.Text ??= "";
|
||||
|
||||
foreach (var spanItem in Regex.Split(span.Text, "( )|([^ ]+)"))
|
||||
{
|
||||
yield return new TextBlockSpan
|
||||
{
|
||||
Text = spanItem,
|
||||
Style = span.Style
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (!RenderingQueue.Any())
|
||||
if (!Items.Any())
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
|
||||
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
||||
|
||||
if (!lines.Any())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
|
||||
var width = lines.Max(x => x.Width);
|
||||
var height = lines.Sum(x => x.LineHeight);
|
||||
|
||||
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
var fullyRenderedItemsCount = lines
|
||||
.SelectMany(x => x.Elements)
|
||||
.GroupBy(x => x.Item)
|
||||
.Count(x => x.Any(y => y.Measurement.IsLast));
|
||||
|
||||
if (fullyRenderedItemsCount == RenderingQueue.Count)
|
||||
if (CurrentElementIndex + lines.Sum(x => x.Elements.Count) == Items.Count)
|
||||
return SpacePlan.FullRender(width, height);
|
||||
|
||||
|
||||
return SpacePlan.PartialRender(width, height);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
||||
|
||||
|
||||
if (!lines.Any())
|
||||
return;
|
||||
|
||||
|
||||
var heightOffset = 0f;
|
||||
var widthOffset = 0f;
|
||||
|
||||
foreach (var line in lines)
|
||||
|
||||
var isLastPart = lines.Sum(x => x.Elements.Count) + CurrentElementIndex == Items.Count;
|
||||
|
||||
foreach (var sourceLine in lines)
|
||||
{
|
||||
widthOffset = 0f;
|
||||
|
||||
var alignmentOffset = GetAlignmentOffset(line.Width);
|
||||
var isLastLine = sourceLine == lines.Last();
|
||||
|
||||
var line = sourceLine;
|
||||
|
||||
if (!(isLastPart && isLastLine))
|
||||
{
|
||||
var spans = sourceLine.Elements
|
||||
.Where(x => x.Item is TextBlockSpan)
|
||||
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
|
||||
.Reverse()
|
||||
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
|
||||
.Reverse()
|
||||
.ToList();
|
||||
|
||||
var wordsWidth = spans
|
||||
.Where(x => !string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
|
||||
.Sum(x => x.Measurement.Width);
|
||||
|
||||
var spaceSpans = spans.Where(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text)).ToList();
|
||||
var spaceWidth = (availableSpace.Width - wordsWidth) / spaceSpans.Count;
|
||||
spaceSpans.ForEach(x => x.Measurement.Width = spaceWidth);
|
||||
|
||||
line = TextLine.From(spans);
|
||||
}
|
||||
|
||||
var alignmentOffset = GetAlignmentOffset(line.Width);
|
||||
|
||||
Canvas.Translate(new Position(alignmentOffset, 0));
|
||||
Canvas.Translate(new Position(0, -line.Ascent));
|
||||
|
||||
|
||||
foreach (var item in line.Elements)
|
||||
{
|
||||
var textDrawingRequest = new TextDrawingRequest
|
||||
{
|
||||
Canvas = Canvas,
|
||||
PageContext = PageContext,
|
||||
|
||||
StartIndex = item.Measurement.StartIndex,
|
||||
EndIndex = item.Measurement.EndIndex,
|
||||
|
||||
TextSize = new Size(item.Measurement.Width, line.LineHeight),
|
||||
TotalAscent = line.Ascent
|
||||
};
|
||||
|
||||
|
||||
item.Item.Draw(textDrawingRequest);
|
||||
|
||||
|
||||
Canvas.Translate(new Position(item.Measurement.Width, 0));
|
||||
widthOffset += item.Measurement.Width;
|
||||
}
|
||||
|
||||
|
||||
Canvas.Translate(new Position(-alignmentOffset, 0));
|
||||
Canvas.Translate(new Position(-line.Width, line.Ascent));
|
||||
Canvas.Translate(new Position(0, line.LineHeight));
|
||||
|
||||
|
||||
heightOffset += line.LineHeight;
|
||||
}
|
||||
|
||||
Canvas.Translate(new Position(0, -heightOffset));
|
||||
|
||||
lines
|
||||
.SelectMany(x => x.Elements)
|
||||
.GroupBy(x => x.Item)
|
||||
.Where(x => x.Any(y => y.Measurement.IsLast))
|
||||
.Select(x => x.Key)
|
||||
.ToList()
|
||||
.ForEach(x => RenderingQueue.Dequeue());
|
||||
|
||||
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
|
||||
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
|
||||
Canvas.Translate(new Position(0, -heightOffset));
|
||||
CurrentElementIndex += lines.Sum(x => x.Elements.Count);
|
||||
|
||||
if (!RenderingQueue.Any())
|
||||
if (CurrentElementIndex == Items.Count)
|
||||
ResetState();
|
||||
|
||||
|
||||
float GetAlignmentOffset(float lineWidth)
|
||||
{
|
||||
if (Alignment == HorizontalAlignment.Left)
|
||||
@@ -130,19 +168,18 @@ namespace QuestPDF.Elements.Text
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
|
||||
private IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
|
||||
{
|
||||
var queue = new Queue<ITextBlockItem>(RenderingQueue);
|
||||
var currentItemIndex = CurrentElementIndex;
|
||||
var currentHeight = 0f;
|
||||
|
||||
while (queue.Any())
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = GetNextLine();
|
||||
|
||||
|
||||
if (!line.Elements.Any())
|
||||
yield break;
|
||||
|
||||
|
||||
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
|
||||
yield break;
|
||||
|
||||
@@ -155,43 +192,29 @@ namespace QuestPDF.Elements.Text
|
||||
var currentWidth = 0f;
|
||||
|
||||
var currentLineElements = new List<TextLineElement>();
|
||||
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (!queue.Any())
|
||||
if (currentItemIndex == Items.Count)
|
||||
break;
|
||||
|
||||
var currentElement = queue.Peek();
|
||||
|
||||
var measurementRequest = new TextMeasurementRequest
|
||||
{
|
||||
Canvas = Canvas,
|
||||
PageContext = PageContext,
|
||||
|
||||
StartIndex = currentItemIndex,
|
||||
AvailableWidth = availableWidth - currentWidth,
|
||||
IsFirstLineElement = !currentLineElements.Any()
|
||||
};
|
||||
|
||||
var measurementResponse = currentElement.Measure(measurementRequest);
|
||||
|
||||
if (measurementResponse == null)
|
||||
var currentElement = Items[currentItemIndex];
|
||||
var textBlockSize = currentElement.Measure();
|
||||
|
||||
if (textBlockSize == null)
|
||||
break;
|
||||
|
||||
if (currentWidth + textBlockSize.Width > availableWidth + Size.Epsilon)
|
||||
break;
|
||||
|
||||
currentLineElements.Add(new TextLineElement
|
||||
{
|
||||
Item = currentElement,
|
||||
Measurement = measurementResponse
|
||||
Measurement = textBlockSize
|
||||
});
|
||||
|
||||
currentWidth += measurementResponse.Width;
|
||||
currentItemIndex = measurementResponse.NextIndex;
|
||||
|
||||
if (!measurementResponse.IsLast)
|
||||
break;
|
||||
|
||||
currentItemIndex = 0;
|
||||
queue.Dequeue();
|
||||
currentWidth += textBlockSize.Width;
|
||||
currentItemIndex ++;
|
||||
}
|
||||
|
||||
return TextLine.From(currentLineElements);
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public static void Placeholder(this IContainer element, string? text = null)
|
||||
{
|
||||
element.Component(new Elements.Placeholder
|
||||
element.Component(new Placeholder
|
||||
{
|
||||
Text = text ?? string.Empty
|
||||
});
|
||||
@@ -99,27 +99,45 @@ namespace QuestPDF.Fluent
|
||||
return element.Element(new Container());
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
||||
public static IContainer ExternalLink(this IContainer element, string url)
|
||||
{
|
||||
return element.Element(new ExternalLink
|
||||
return element.Hyperlink(url);
|
||||
}
|
||||
|
||||
public static IContainer Hyperlink(this IContainer element, string url)
|
||||
{
|
||||
return element.Element(new Hyperlink
|
||||
{
|
||||
Url = url
|
||||
});
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Section method.")]
|
||||
public static IContainer Location(this IContainer element, string locationName)
|
||||
{
|
||||
return element.Element(new InternalLocation
|
||||
return element.Section(locationName);
|
||||
}
|
||||
|
||||
public static IContainer Section(this IContainer element, string sectionName)
|
||||
{
|
||||
return element.Element(new Section
|
||||
{
|
||||
LocationName = locationName
|
||||
LocationName = sectionName
|
||||
});
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
||||
public static IContainer InternalLink(this IContainer element, string locationName)
|
||||
{
|
||||
return element.Element(new InternalLink
|
||||
return element.SectionLink(locationName);
|
||||
}
|
||||
|
||||
public static IContainer SectionLink(this IContainer element, string sectionName)
|
||||
{
|
||||
return element.Element(new SectionLink
|
||||
{
|
||||
LocationName = locationName
|
||||
SectionName = sectionName
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public class Document : IDocument
|
||||
{
|
||||
private Action<IDocumentContainer> ContentSource { get; }
|
||||
private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default;
|
||||
|
||||
private Document(Action<IDocumentContainer> contentSource)
|
||||
{
|
||||
ContentSource = contentSource;
|
||||
}
|
||||
|
||||
public static Document Create(Action<IDocumentContainer> handler)
|
||||
{
|
||||
return new Document(handler);
|
||||
}
|
||||
|
||||
public Document WithMetadata(DocumentMetadata metadata)
|
||||
{
|
||||
Metadata = metadata ?? Metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
#region IDocument
|
||||
|
||||
public DocumentMetadata GetMetadata() => Metadata;
|
||||
public void Compose(IDocumentContainer container) => ContentSource(container);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -90,57 +90,81 @@ namespace QuestPDF.Fluent
|
||||
Span(Environment.NewLine);
|
||||
}
|
||||
|
||||
private void PageNumber(string slotName, TextStyle? style = null)
|
||||
private static Func<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
|
||||
|
||||
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
|
||||
{
|
||||
if (IsNullOrEmpty(slotName))
|
||||
throw new ArgumentException(nameof(slotName));
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
format ??= DefaultTextFormat;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockPageNumber()
|
||||
{
|
||||
Style = style,
|
||||
SlotName = slotName
|
||||
Source = context => format(pageNumber(context)),
|
||||
Style = style
|
||||
});
|
||||
}
|
||||
|
||||
public void CurrentPageNumber(TextStyle? style = null)
|
||||
|
||||
public void CurrentPageNumber(TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(PageContext.CurrentPageSlot, style);
|
||||
PageNumber(x => x.CurrentPage, style, format);
|
||||
}
|
||||
|
||||
public void TotalPages(TextStyle? style = null)
|
||||
public void TotalPages(TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(PageContext.TotalPagesSlot, style);
|
||||
PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format);
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
|
||||
public void PageNumberOfLocation(string locationName, TextStyle? style = null)
|
||||
{
|
||||
if (IsNullOrEmpty(locationName))
|
||||
throw new ArgumentException(nameof(locationName));
|
||||
|
||||
PageNumber(locationName, style);
|
||||
BeginPageNumberOfSection(locationName);
|
||||
}
|
||||
|
||||
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
|
||||
public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.PageStart, style, format);
|
||||
}
|
||||
|
||||
public void EndPageNumberOfSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.PageEnd, style, format);
|
||||
}
|
||||
|
||||
public void PageNumberWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd, style, format);
|
||||
}
|
||||
|
||||
public void TotalPagesWithinSection(string locationName, TextStyle? style = null, Func<int?, string>? format = null)
|
||||
{
|
||||
PageNumber(x => x.GetLocation(locationName)?.Length, style, format);
|
||||
}
|
||||
|
||||
public void SectionLink(string? text, string sectionName, TextStyle? style = null)
|
||||
{
|
||||
if (IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (IsNullOrEmpty(locationName))
|
||||
throw new ArgumentException(nameof(locationName));
|
||||
if (IsNullOrEmpty(sectionName))
|
||||
throw new ArgumentException(nameof(sectionName));
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockInternalLink
|
||||
AddItemToLastTextBlock(new TextBlockSectionLink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
LocationName = locationName
|
||||
SectionName = sectionName
|
||||
});
|
||||
}
|
||||
|
||||
public void ExternalLocation(string? text, string url, TextStyle? style = null)
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
||||
public void InternalLocation(string? text, string locationName, TextStyle? style = null)
|
||||
{
|
||||
SectionLink(text, locationName, style);
|
||||
}
|
||||
|
||||
public void Hyperlink(string? text, string url, TextStyle? style = null)
|
||||
{
|
||||
if (IsNullOrEmpty(text))
|
||||
return;
|
||||
@@ -150,7 +174,7 @@ namespace QuestPDF.Fluent
|
||||
|
||||
style ??= TextStyle.Default;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockExternalLink
|
||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
@@ -158,6 +182,12 @@ namespace QuestPDF.Fluent
|
||||
});
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
||||
public void ExternalLocation(string? text, string url, TextStyle? style = null)
|
||||
{
|
||||
Hyperlink(text, url, style);
|
||||
}
|
||||
|
||||
public IContainer Element()
|
||||
{
|
||||
var container = new Container();
|
||||
|
||||
@@ -8,11 +8,12 @@ namespace QuestPDF.Infrastructure
|
||||
|
||||
void DrawRectangle(Position vector, Size size, string color);
|
||||
void DrawText(string text, Position position, TextStyle style);
|
||||
void DrawShapedText(string text, Position position, TextStyle style);
|
||||
void DrawImage(SKImage image, Position position, Size size);
|
||||
|
||||
void DrawExternalLink(string url, Size size);
|
||||
void DrawLocationLink(string locationName, Size size);
|
||||
void DrawLocation(string locationName);
|
||||
void DrawHyperlink(string url, Size size);
|
||||
void DrawSectionLink(string sectionName, Size size);
|
||||
void DrawSection(string sectionName);
|
||||
|
||||
void Rotate(float angle);
|
||||
void Scale(float scaleX, float scaleY);
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public interface IPageContext
|
||||
internal class DocumentLocation
|
||||
{
|
||||
void SetLocationPage(string key);
|
||||
int GetLocationPage(string key);
|
||||
ICollection<string> GetRegisteredLocations();
|
||||
public string Name { get; set; }
|
||||
public int PageStart { get; set; }
|
||||
public int PageEnd { get; set; }
|
||||
public int Length => PageEnd - PageStart + 1;
|
||||
}
|
||||
|
||||
internal interface IPageContext
|
||||
{
|
||||
int CurrentPage { get; }
|
||||
void SetSectionPage(string name);
|
||||
DocumentLocation? GetLocation(string name);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public class PageContext : IPageContext
|
||||
internal class PageContext : IPageContext
|
||||
{
|
||||
public const string CurrentPageSlot = "currentPage";
|
||||
public const string TotalPagesSlot = "totalPages";
|
||||
public const string DocumentLocation = "document";
|
||||
|
||||
private Dictionary<string, int> Locations { get; } = new Dictionary<string, int>();
|
||||
private int PageNumber { get; set; }
|
||||
private List<DocumentLocation> Locations { get; } = new();
|
||||
public int CurrentPage { get; private set; }
|
||||
|
||||
internal void SetPageNumber(int number)
|
||||
{
|
||||
PageNumber = number;
|
||||
Locations[CurrentPageSlot] = number;
|
||||
|
||||
if (!Locations.ContainsKey(TotalPagesSlot) || Locations[TotalPagesSlot] < number)
|
||||
Locations[TotalPagesSlot] = number;
|
||||
CurrentPage = number;
|
||||
SetSectionPage(DocumentLocation);
|
||||
}
|
||||
|
||||
public void SetLocationPage(string key)
|
||||
public void SetSectionPage(string name)
|
||||
{
|
||||
if (Locations.ContainsKey(key))
|
||||
return;
|
||||
|
||||
Locations[key] = PageNumber;
|
||||
var location = GetLocation(name);
|
||||
|
||||
if (location == null)
|
||||
{
|
||||
location = new DocumentLocation
|
||||
{
|
||||
Name = name,
|
||||
PageStart = CurrentPage,
|
||||
PageEnd = CurrentPage
|
||||
};
|
||||
|
||||
Locations.Add(location);
|
||||
}
|
||||
|
||||
if (location.PageEnd < CurrentPage)
|
||||
location.PageEnd = CurrentPage;
|
||||
}
|
||||
|
||||
public int GetLocationPage(string key)
|
||||
public DocumentLocation? GetLocation(string name)
|
||||
{
|
||||
if (!Locations.ContainsKey(key))
|
||||
throw new ArgumentException($"The location '{key}' does not exists.");
|
||||
|
||||
return Locations[key];
|
||||
}
|
||||
|
||||
public ICollection<string> GetRegisteredLocations()
|
||||
{
|
||||
return Locations.Keys;
|
||||
return Locations.FirstOrDefault(x => x.Name == name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,8 @@ namespace QuestPDF.Infrastructure
|
||||
internal bool? HasStrikethrough { get; set; }
|
||||
internal bool? HasUnderline { get; set; }
|
||||
|
||||
internal string? Key { get; private set; }
|
||||
internal object PaintKey { get; private set; }
|
||||
internal object FontMetricsKey { get; private set; }
|
||||
|
||||
internal static TextStyle LibraryDefault => new TextStyle
|
||||
{
|
||||
@@ -42,7 +43,8 @@ namespace QuestPDF.Infrastructure
|
||||
HasGlobalStyleApplied = true;
|
||||
|
||||
ApplyParentStyle(globalStyle);
|
||||
Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}";
|
||||
PaintKey ??= (FontType, Size, FontWeight, IsItalic, Color);
|
||||
FontMetricsKey ??= (FontType, Size, FontWeight, IsItalic);
|
||||
}
|
||||
|
||||
internal void ApplyParentStyle(TextStyle parentStyle)
|
||||
|
||||
@@ -13,6 +13,10 @@ namespace QuestPDF.Infrastructure
|
||||
|
||||
Feet,
|
||||
Inch,
|
||||
|
||||
/// <summary>
|
||||
/// 1/1000th of inch
|
||||
/// </summary>
|
||||
Mill
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user