Compare commits
10 Commits
| 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace QuestPDF.Examples
|
||||
.PageSize(PageSizes.A4)
|
||||
.ProducePdf()
|
||||
.ShowResults()
|
||||
.RenderDocument(x => ComposeBook(x, chapters));
|
||||
.Render(x => ComposeBook(x, chapters));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -42,8 +42,8 @@ namespace QuestPDF.Examples
|
||||
RenderingTest
|
||||
.Create()
|
||||
.PageSize(PageSizes.A4)
|
||||
.ProducePdf()
|
||||
.RenderDocument(x => ComposeBook(x, chapters));
|
||||
.ProducePdf()
|
||||
.Render(x => ComposeBook(x, chapters));
|
||||
}
|
||||
|
||||
IEnumerable<float> PerformTest(int attempts)
|
||||
@@ -102,30 +102,37 @@ namespace QuestPDF.Examples
|
||||
}
|
||||
}
|
||||
|
||||
private void ComposeBook(IDocumentContainer container, ICollection<BookChapter> chapters)
|
||||
private void ComposeBook(IContainer container, ICollection<BookChapter> chapters)
|
||||
{
|
||||
var subtitleStyle = TextStyle.Default.Size(24).SemiBold().Color(Colors.Blue.Medium);
|
||||
var normalStyle = TextStyle.Default.Size(14);
|
||||
|
||||
ComposePage(container);
|
||||
|
||||
container.Page(page =>
|
||||
void ComposePage(IContainer container)
|
||||
{
|
||||
page.Margin(50);
|
||||
|
||||
page.Content().Column(column =>
|
||||
{
|
||||
column.Item().Element(Title);
|
||||
column.Item().PageBreak();
|
||||
column.Item().Element(TableOfContents);
|
||||
column.Item().PageBreak();
|
||||
container
|
||||
.Padding(50)
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
decoration
|
||||
.Content()
|
||||
.Column(column =>
|
||||
{
|
||||
column.Item().Element(Title);
|
||||
column.Item().PageBreak();
|
||||
column.Item().Element(TableOfContents);
|
||||
column.Item().PageBreak();
|
||||
|
||||
Chapters(column);
|
||||
Chapters(column);
|
||||
|
||||
column.Item().Element(Acknowledgements);
|
||||
});
|
||||
|
||||
page.Footer().Element(Footer);
|
||||
});
|
||||
column.Item().Element(Acknowledgements);
|
||||
});
|
||||
|
||||
decoration.After().Element(Footer);
|
||||
});
|
||||
}
|
||||
|
||||
void Title(IContainer container)
|
||||
{
|
||||
container
|
||||
|
||||
@@ -31,8 +31,7 @@ namespace QuestPDF.Examples
|
||||
{
|
||||
text.DefaultTextStyle(TextStyle.Default.Size(20));
|
||||
text.Span("This is a normal text, followed by an ");
|
||||
text.Span("underlined red text", TextStyle.Default.Color(Colors.Red.Medium).Underline());
|
||||
text.Span(".");
|
||||
text.Span("underlined red text.", TextStyle.Default.Size(20).Color(Colors.Red.Medium).Underline());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -190,7 +189,7 @@ namespace QuestPDF.Examples
|
||||
text.EmptyLine();
|
||||
|
||||
text.Span("The new text element also supports injecting custom content between words: ");
|
||||
text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image);
|
||||
text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
|
||||
text.Span(".");
|
||||
|
||||
text.EmptyLine();
|
||||
@@ -237,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);
|
||||
|
||||
@@ -247,12 +246,12 @@ namespace QuestPDF.Examples
|
||||
text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16));
|
||||
|
||||
text.Span("The new text element also supports injecting custom content between words: ");
|
||||
text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image);
|
||||
text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image);
|
||||
text.Span(".");
|
||||
|
||||
text.EmptyLine();
|
||||
|
||||
foreach (var i in Enumerable.Range(1, 100))
|
||||
foreach (var i in Enumerable.Range(1, 1000))
|
||||
{
|
||||
text.Line($"{i}: {Placeholders.Paragraph()}");
|
||||
|
||||
@@ -268,80 +267,5 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MeasureIssueWhenSpaceAtLineEnd()
|
||||
{
|
||||
// issue 135
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.Background(Colors.White);
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Text(
|
||||
"This is a specially crafted sentence with a specially chosen length for demonstration of the bug that occurs ;;;;;. ",
|
||||
TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EmptyText()
|
||||
{
|
||||
// issue 135
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.Background(Colors.White);
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Text(
|
||||
" ",
|
||||
TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Whitespaces()
|
||||
{
|
||||
// issue 135
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.Background(Colors.White);
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Text(
|
||||
" x ",
|
||||
TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -17,30 +17,19 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
internal static void GeneratePdf(Stream stream, IDocument document)
|
||||
{
|
||||
CheckIfStreamIsCompatible(stream);
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new PdfCanvas(stream, metadata);
|
||||
var writeOnlyStream = new WriteOnlyStream(stream);
|
||||
var canvas = new PdfCanvas(writeOnlyStream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
}
|
||||
|
||||
internal static void GenerateXps(Stream stream, IDocument document)
|
||||
{
|
||||
CheckIfStreamIsCompatible(stream);
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new XpsCanvas(stream, metadata);
|
||||
var writeOnlyStream = new WriteOnlyStream(stream);
|
||||
var canvas = new XpsCanvas(writeOnlyStream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
}
|
||||
|
||||
private static void CheckIfStreamIsCompatible(Stream stream)
|
||||
{
|
||||
if (!stream.CanWrite)
|
||||
throw new ArgumentException("The library requires a Stream object with the 'write' capability available (the CanWrite flag). Please consider using the MemoryStream class.");
|
||||
|
||||
if (!stream.CanSeek)
|
||||
throw new ArgumentException("The library requires a Stream object with the 'seek' capability available (the CanSeek flag). Please consider using the MemoryStream class.");
|
||||
}
|
||||
|
||||
internal static ICollection<byte[]> GenerateImages(IDocument document)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,16 +6,16 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
internal class SkiaDocumentCanvasBase : SkiaCanvasBase
|
||||
{
|
||||
private SKDocument? Document { get; }
|
||||
private SKDocument Document { get; }
|
||||
|
||||
protected SkiaDocumentCanvasBase(SKDocument document)
|
||||
public SkiaDocumentCanvasBase(SKDocument document)
|
||||
{
|
||||
Document = document;
|
||||
}
|
||||
|
||||
~SkiaDocumentCanvasBase()
|
||||
{
|
||||
Document?.Dispose();
|
||||
Document.Dispose();
|
||||
}
|
||||
|
||||
public override void BeginDocument()
|
||||
@@ -25,7 +25,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
public override void EndDocument()
|
||||
{
|
||||
Canvas?.Dispose();
|
||||
Canvas.Dispose();
|
||||
|
||||
Document.Close();
|
||||
Document.Dispose();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -25,8 +25,8 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
|
||||
LineType.Horizontal when availableSpace.Height + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(0, Size),
|
||||
LineType.Vertical when availableSpace.Width >= Size => SpacePlan.FullRender(Size, 0),
|
||||
LineType.Horizontal when availableSpace.Height >= Size => SpacePlan.FullRender(0, Size),
|
||||
_ => SpacePlan.Wrap()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,10 +17,8 @@ namespace QuestPDF.Elements
|
||||
if (perfectScale == null)
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
var scaledSpace = ScaleSize(availableSpace, 1 / perfectScale.Value);
|
||||
var childSizeInScale = Child.Measure(scaledSpace);
|
||||
var childSizeInOriginalScale = ScaleSize(childSizeInScale, perfectScale.Value);
|
||||
return SpacePlan.FullRender(childSizeInOriginalScale);
|
||||
var targetSpace = ScaleSize(availableSpace, perfectScale.Value);
|
||||
return SpacePlan.FullRender(targetSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size 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 => NextIndex == EndIndex;
|
||||
}
|
||||
}
|
||||
@@ -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 IsFirstElementInLine { 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,135 +1,88 @@
|
||||
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 ();
|
||||
|
||||
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 the element is the first one within the line,
|
||||
// ignore leading spaces
|
||||
if (request.IsFirstElementInLine)
|
||||
{
|
||||
while (startIndex < Text.Length && Text[startIndex] == space)
|
||||
startIndex++;
|
||||
}
|
||||
// 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);
|
||||
|
||||
if (Text.Length == 0 || startIndex == Text.Length)
|
||||
{
|
||||
return new TextMeasurementResult
|
||||
{
|
||||
Width = 0,
|
||||
|
||||
LineHeight = Style.LineHeight ?? 1,
|
||||
Ascent = fontMetrics.Ascent,
|
||||
Descent = fontMetrics.Descent
|
||||
};
|
||||
}
|
||||
// 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;
|
||||
|
||||
// start breaking text from requested position
|
||||
var text = Text.AsSpan().Slice(startIndex);
|
||||
|
||||
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
|
||||
|
||||
if (textLength <= 0)
|
||||
return null;
|
||||
|
||||
// break text only on spaces
|
||||
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
|
||||
|
||||
if (wrappedTextLength == null)
|
||||
return null;
|
||||
|
||||
textLength = wrappedTextLength.Value;
|
||||
|
||||
text = text.Slice(0, textLength);
|
||||
|
||||
var endIndex = startIndex + textLength;
|
||||
var nextIndex = endIndex;
|
||||
|
||||
// when breaking text, omit spaces at the end of the line
|
||||
while (nextIndex < 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
|
||||
};
|
||||
|
||||
static int? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
|
||||
{
|
||||
// textLength - length of the part of the text that fits in available width (creating a line)
|
||||
|
||||
// entire text fits, no need to wrap
|
||||
if (textLength == text.Length)
|
||||
return textLength;
|
||||
|
||||
// current line ends at word, next character is space, perfect place to wrap
|
||||
if (text[textLength - 1] != space && text[textLength] == space)
|
||||
return textLength;
|
||||
|
||||
// find last space within the available text to wrap
|
||||
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space);
|
||||
|
||||
// text contains space that can be used to wrap
|
||||
if (lastSpaceIndex > 0)
|
||||
return lastSpaceIndex;
|
||||
|
||||
// there is no available space to wrap text
|
||||
// if the item is first within the line, perform safe mode and chop the word
|
||||
// otherwise, move the item into the next line
|
||||
return isFirstElementInLine ? textLength : null;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -141,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,
|
||||
IsFirstElementInLine = !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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -29,8 +29,6 @@ namespace QuestPDF.Fluent
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XPS
|
||||
|
||||
public static byte[] GenerateXps(this IDocument document)
|
||||
{
|
||||
@@ -50,6 +48,8 @@ namespace QuestPDF.Fluent
|
||||
DocumentGenerator.GenerateXps(stream, document);
|
||||
}
|
||||
|
||||
#region XPS
|
||||
|
||||
#endregion
|
||||
|
||||
#region Images
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace QuestPDF.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// SkiaSharp calls the Position property when generating target document file.
|
||||
/// If the output stream does not support the Position property, the NullReferenceException is thrown.
|
||||
/// This wrapper fixes this issue by providing cached Position value (always the end of the stream / current length).
|
||||
/// Example stream affected: HttpContext.Response.Body
|
||||
/// </summary>
|
||||
internal class WriteOnlyStream : Stream
|
||||
{
|
||||
private readonly Stream InnerStream;
|
||||
private long StreamLength { get; set; }
|
||||
|
||||
public WriteOnlyStream(Stream stream)
|
||||
{
|
||||
if (!stream.CanWrite)
|
||||
throw new NotSupportedException("Stream cannot be written");
|
||||
|
||||
InnerStream = stream;
|
||||
}
|
||||
|
||||
public override bool CanRead => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => StreamLength;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => StreamLength;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Flush() => InnerStream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override void SetLength(long value)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
InnerStream.Write(buffer, offset, count);
|
||||
StreamLength += count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2022.2.7</Version>
|
||||
<Version>2022.2.3</Version>
|
||||
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>9</LangVersion>
|
||||
@@ -18,13 +18,14 @@
|
||||
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
|
||||
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0</TargetFrameworks>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
||||
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
<a href="https://www.questpdf.com/" target="_blank">
|
||||
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="300">
|
||||
<a href="https://www.questpdf.com/">
|
||||
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/logo.svg" width="400px">
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
[](https://github.com/QuestPDF/QuestPDF/stargazers)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
|
||||
[](https://github.com/sponsors/QuestPDF)
|
||||
|
||||
QuestPDF presents a new approach to PDF document generation. Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements. Then, everything is rendered using the SkiaSharp library (a Skia port for .NET, used in Chrome, Android, MAUI, etc.).
|
||||
|
||||
I have designed this layouting engine with full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behaviour of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
|
||||
|
||||
Reference in New Issue
Block a user