Compare commits

...

11 Commits

Author SHA1 Message Date
MarcinZiabek cbaebdd670 Rendering text paragraphs without the Column element (not fully working) 2022-08-20 19:35:24 +02:00
MarcinZiabek 52ad0f5c24 2202.8.2 2022-08-20 18:22:06 +02:00
Marcin Ziąbek 53107073a6 Merge pull request #322 from emanueleguastella/main
Fix DocumentLayoutException when creating a document with more than one page
2022-08-20 18:14:08 +02:00
Emanuele Guastella c71bb3ea59 Fix DocumentLayoutException when creating a document with more than one page 2022-08-20 14:17:52 +02:00
Marcin Ziąbek 6cced3c143 Update readme.md 2022-08-19 14:45:26 +02:00
MarcinZiabek 425ea59cfe 2022.8.1 2022-08-19 14:40:49 +02:00
MarcinZiabek 4d326496c6 Optimization for the Column element: do not measure child when available height is negative 2022-08-17 15:14:38 +02:00
MarcinZiabek b7b6488d16 Updated stability of rendering elements when negative space 2022-08-17 15:13:06 +02:00
MarcinZiabek f0ba5fc32d Fixed: page breaking rendering does not work in very specific corner cases 2022-08-16 23:59:05 +02:00
MarcinZiabek 04da32e0e7 Stability improvements for text wrapping 2022-08-16 16:33:37 +02:00
MarcinZiabek 5bdb338996 Fixed: default text style does not always work 2022-08-16 16:25:39 +02:00
18 changed files with 183 additions and 68 deletions
+3
View File
@@ -129,6 +129,9 @@ namespace QuestPDF.Drawing
{ {
if (Glyphs.Length == 0) if (Glyphs.Length == 0)
return null; return null;
if (startIndex > endIndex)
return null;
using var skTextBlobBuilder = new SKTextBlobBuilder(); using var skTextBlobBuilder = new SKTextBlobBuilder();
+4 -1
View File
@@ -1,4 +1,5 @@
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using SkiaSharp; using SkiaSharp;
@@ -12,7 +13,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
return SpacePlan.FullRender(availableSpace); return availableSpace.IsNegative()
? SpacePlan.Wrap()
: SpacePlan.FullRender(availableSpace);
} }
internal override void Draw(Size availableSpace) internal override void Draw(Size availableSpace)
+6 -1
View File
@@ -94,7 +94,12 @@ namespace QuestPDF.Elements
if (item.IsRendered) if (item.IsRendered)
continue; continue;
var itemSpace = new Size(availableSpace.Width, availableSpace.Height - topOffset); var availableHeight = availableSpace.Height - topOffset;
if (availableHeight < 0)
break;
var itemSpace = new Size(availableSpace.Width, availableHeight);
var measurement = item.Measure(itemSpace); var measurement = item.Measure(itemSpace);
if (measurement.Type == SpacePlanType.Wrap) if (measurement.Type == SpacePlanType.Wrap)
+4 -1
View File
@@ -1,5 +1,6 @@
using System; using System;
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using SkiaSharp; using SkiaSharp;
@@ -11,7 +12,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
return SpacePlan.FullRender(availableSpace.Width, availableSpace.Height); return availableSpace.IsNegative()
? SpacePlan.Wrap()
: SpacePlan.FullRender(availableSpace);
} }
internal override void Draw(Size availableSpace) internal override void Draw(Size availableSpace)
+4 -1
View File
@@ -1,4 +1,5 @@
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
@@ -9,7 +10,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
return SpacePlan.FullRender(0, 0); return availableSpace.IsNegative()
? SpacePlan.Wrap()
: SpacePlan.FullRender(0, 0);
} }
internal override void Draw(Size availableSpace) internal override void Draw(Size availableSpace)
+4 -1
View File
@@ -1,4 +1,5 @@
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using SkiaSharp; using SkiaSharp;
@@ -15,7 +16,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
return SpacePlan.FullRender(availableSpace); return availableSpace.IsNegative()
? SpacePlan.Wrap()
: SpacePlan.FullRender(availableSpace);
} }
internal override void Draw(Size availableSpace) internal override void Draw(Size availableSpace)
+3
View File
@@ -23,6 +23,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
if (availableSpace.IsNegative())
return SpacePlan.Wrap();
return Type switch return Type switch
{ {
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0), LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
+4
View File
@@ -1,4 +1,5 @@
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
@@ -14,6 +15,9 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
{ {
if (availableSpace.IsNegative())
return SpacePlan.Wrap();
if (IsRendered) if (IsRendered)
return SpacePlan.FullRender(0, 0); return SpacePlan.FullRender(0, 0);
@@ -19,5 +19,6 @@ namespace QuestPDF.Elements.Text.Calculation
public int TotalIndex { get; set; } public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex; public bool IsLast => EndIndex == TotalIndex;
public bool IsNewLine { get; set; }
} }
} }
+26 -6
View File
@@ -39,18 +39,34 @@ namespace QuestPDF.Elements.Text.Items
var paint = Style.ToPaint(); var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics(); var fontMetrics = Style.ToFontMetrics();
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
// if the element is the first one within the line,
// ignore leading spaces and new lines
var spaceCodepoint = paint.ToFont().Typeface.GetGlyph(' ');
var newLineCodepoint = paint.ToFont().Typeface.GetGlyph('\n');
var returnCodepoint = paint.ToFont().Typeface.GetGlyph('\r');
var startIndex = request.StartIndex; var startIndex = request.StartIndex;
// if the element is the first one within the line,
// ignore leading spaces
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine) if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{ {
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint) while (startIndex < TextShapingResult.Glyphs.Length && (Text[startIndex] == spaceCodepoint || Text[startIndex] == newLineCodepoint || Text[startIndex] == returnCodepoint))
startIndex++; startIndex++;
} }
// calculate max index (new new line)
var newLineIndex = startIndex;
while (newLineIndex < TextShapingResult.Glyphs.Length)
{
var glyphCodepoint = TextShapingResult.Glyphs[newLineIndex].Codepoint;
if (glyphCodepoint == newLineCodepoint || glyphCodepoint == returnCodepoint)
break;
newLineIndex++;
}
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length) if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
{ {
return new TextMeasurementResult return new TextMeasurementResult
@@ -66,8 +82,10 @@ namespace QuestPDF.Elements.Text.Items
// start breaking text from requested position // start breaking text from requested position
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth); var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
if (endIndex < 0) if (endIndex < startIndex)
return null; return null;
endIndex = Math.Min(endIndex, newLineIndex);
// break text only on spaces // break text only on spaces
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine); var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
@@ -90,7 +108,9 @@ namespace QuestPDF.Elements.Text.Items
StartIndex = startIndex, StartIndex = startIndex,
EndIndex = wrappedText.Value.endIndex, EndIndex = wrappedText.Value.endIndex,
NextIndex = wrappedText.Value.nextIndex, NextIndex = wrappedText.Value.nextIndex,
TotalIndex = TextShapingResult.Glyphs.Length - 1 TotalIndex = TextShapingResult.Glyphs.Length - 1,
IsNewLine = endIndex == newLineIndex
}; };
} }
+8 -1
View File
@@ -11,7 +11,8 @@ namespace QuestPDF.Elements.Text
internal class TextBlock : Element, IStateResettable internal class TextBlock : Element, IStateResettable
{ {
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>(); public List<ITextBlockItem> Items { get; set; } = new();
public float ParagraphSpacing { get; set; } = 0;
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text)); public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
@@ -110,6 +111,12 @@ namespace QuestPDF.Elements.Text
Canvas.Translate(new Position(0, line.LineHeight)); Canvas.Translate(new Position(0, line.LineHeight));
heightOffset += line.LineHeight; heightOffset += line.LineHeight;
if (line.Elements.Last().Measurement.IsNewLine)
{
heightOffset += ParagraphSpacing;
Canvas.Translate(new Position(0, ParagraphSpacing));
}
} }
Canvas.Translate(new Position(0, -heightOffset)); Canvas.Translate(new Position(0, -heightOffset));
+14 -51
View File
@@ -40,10 +40,9 @@ namespace QuestPDF.Fluent
public class TextDescriptor public class TextDescriptor
{ {
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>(); internal TextBlock TextBlock = new();
private TextStyle DefaultStyle { get; set; } = TextStyle.Default; private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
private float Spacing { get; set; } = 0f;
public void DefaultTextStyle(TextStyle style) public void DefaultTextStyle(TextStyle style)
{ {
@@ -72,17 +71,9 @@ namespace QuestPDF.Fluent
public void ParagraphSpacing(float value, Unit unit = Unit.Point) public void ParagraphSpacing(float value, Unit unit = Unit.Point)
{ {
Spacing = value.ToPoints(unit); TextBlock.ParagraphSpacing = value.ToPoints(unit);
} }
private void AddItemToLastTextBlock(ITextBlockItem item)
{
if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock());
TextBlocks.Last().Items.Add(item);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")] [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
public void Span(string? text, TextStyle style) public void Span(string? text, TextStyle style)
{ {
@@ -96,28 +87,13 @@ namespace QuestPDF.Fluent
if (text == null) if (text == null)
return descriptor; return descriptor;
var items = text TextBlock.Items.Add(new TextBlockSpan
.Replace("\r", string.Empty) {
.Split(new[] { '\n' }, StringSplitOptions.None) Text = text,
.Select(x => new TextBlockSpan Style = style
{ });
Text = x,
Style = style
})
.ToList();
AddItemToLastTextBlock(items.First());
items
.Skip(1)
.Select(x => new TextBlock
{
Items = new List<ITextBlockItem> { x }
})
.ToList()
.ForEach(TextBlocks.Add);
return descriptor; return descriptor;
} }
@@ -137,7 +113,7 @@ namespace QuestPDF.Fluent
var style = DefaultStyle.Clone(); var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(style); var descriptor = new TextPageNumberDescriptor(style);
AddItemToLastTextBlock(new TextBlockPageNumber TextBlock.Items.Add(new TextBlockPageNumber
{ {
Source = context => descriptor.FormatFunction(pageNumber(context)), Source = context => descriptor.FormatFunction(pageNumber(context)),
Style = style Style = style
@@ -193,7 +169,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text)) if (IsNullOrEmpty(text))
return descriptor; return descriptor;
AddItemToLastTextBlock(new TextBlockSectionLink TextBlock.Items.Add(new TextBlockSectionLink
{ {
Style = style, Style = style,
Text = text, Text = text,
@@ -220,7 +196,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text)) if (IsNullOrEmpty(text))
return descriptor; return descriptor;
AddItemToLastTextBlock(new TextBlockHyperlink TextBlock.Items.Add(new TextBlockHyperlink
{ {
Style = style, Style = style,
Text = text, Text = text,
@@ -240,26 +216,13 @@ namespace QuestPDF.Fluent
{ {
var container = new Container(); var container = new Container();
AddItemToLastTextBlock(new TextBlockElement TextBlock.Items.Add(new TextBlockElement
{ {
Element = container Element = container
}); });
return container.AlignBottom().MinimalBox(); return container.AlignBottom().MinimalBox();
} }
internal void Compose(IContainer container)
{
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
container.DefaultTextStyle(DefaultStyle).Column(column =>
{
column.Spacing(Spacing);
foreach (var textBlock in TextBlocks)
column.Item().Element(textBlock);
});
}
} }
public static class TextExtensions public static class TextExtensions
@@ -272,7 +235,7 @@ namespace QuestPDF.Fluent
descriptor.Alignment = alignment.Horizontal; descriptor.Alignment = alignment.Horizontal;
content?.Invoke(descriptor); content?.Invoke(descriptor);
descriptor.Compose(element); element.Element(descriptor.TextBlock);
} }
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")] [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
+6
View File
@@ -4,6 +4,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
namespace QuestPDF.Helpers namespace QuestPDF.Helpers
@@ -54,5 +55,10 @@ namespace QuestPDF.Helpers
handler(element); handler(element);
} }
internal static bool IsNegative(this Size size)
{
return size.Width < 0f || size.Height < 0f;
}
} }
} }
+2 -4
View File
@@ -40,10 +40,8 @@ namespace QuestPDF.Infrastructure
WrapAnywhere = false WrapAnywhere = false
}; };
// REVIEW: Should this be a method call that news up a TextStyle, // it is important to create new instances for the DefaultTextStyle element to work correctly
// or can it be a static variable? public static TextStyle Default => new TextStyle();
// (style mutations seem to create a clone anyway)
public static readonly TextStyle Default = new TextStyle();
internal void ApplyGlobalStyle(TextStyle globalStyle) internal void ApplyGlobalStyle(TextStyle globalStyle)
{ {
+1 -1
View File
@@ -3,7 +3,7 @@
<Authors>MarcinZiabek</Authors> <Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company> <Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId> <PackageId>QuestPDF</PackageId>
<Version>2022.8.0</Version> <Version>2202.8.2</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> <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> <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
+78
View File
@@ -0,0 +1,78 @@
[![Dotnet](https://img.shields.io/badge/platform-.NET-blue)](https://www.nuget.org/packages/QuestPDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/stargazers)
[![Nuget version](https://img.shields.io/nuget/v/QuestPdf)](https://www.nuget.org/packages/QuestPDF/)
[![Nuget download](https://img.shields.io/nuget/dt/QuestPDF)](https://www.nuget.org/packages/QuestPDF/)
[![License](https://img.shields.io/github/license/QuestPDF/QuestPDF)](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
[![Sponsor project](https://img.shields.io/badge/sponsor-project-red)](https://github.com/sponsors/QuestPDF)
QuestPDF is an open-source .NET library for PDF documents generation.
It offers a layout engine designed with a 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 behavior 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.
## Documentation
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](https://www.questpdf.com/getting-started.html)
A short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
[![API reference](https://img.shields.io/badge/%F0%9F%93%96%20read-API%20reference-blue)](https://www.questpdf.com/api-reference/index.html)
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/design-patterns.html)
Everything that may help you designing great reports and create reusable code that is easy to maintain.
## Simplicity is the key
How easy it is to start and prototype with QuestPDF? Really easy thanks to its minimal API! Please analyse the code below:
```#
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
// code in your main method
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.GeneratePdf("hello.pdf");
```
And compare it to the produced PDF file:
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/docs/public/minimal-example-shadow.png)
## Are you ready for more?
The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice).
![invoice](https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/docs/public/invoice-small.png)
+13
View File
@@ -1,3 +1,5 @@
2022.8.0:
- Improved library performance, - Improved library performance,
- Breaking change: changed default font from Calibri to an open-source Lato, - Breaking change: changed default font from Calibri to an open-source Lato,
- Default font files are included with the nuget package, making it safe to deploy on any environment, - Default font files are included with the nuget package, making it safe to deploy on any environment,
@@ -5,3 +7,14 @@
- When requested font is not available on the runtime environment, library provides list of available fonts, - When requested font is not available on the runtime environment, library provides list of available fonts,
- Fixed a rare layout overflow exception with the Inlined element, - Fixed a rare layout overflow exception with the Inlined element,
- Fixed a memory leak connected to the HarfBuzz library. - Fixed a memory leak connected to the HarfBuzz library.
2022.8.1:
- Fixed: default text style does not always work
- Fixed: page breaking rendering does not work in very specific corner cases
- Stability improvements for text wrapping
- Updated stability of rendering elements in negative space
- Optimization for the Column element: do not measure child when available height is negative
2202.8.2
- Fixed: the Column element incorrectly renders zero-height elements.
+2
View File
@@ -23,6 +23,8 @@ Choosing a project dependency could be difficult. We need to ensure stability an
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐ ⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
<img src="https://user-images.githubusercontent.com/9263853/184642026-27dd7567-a46a-45d4-9594-e6a70a7193e9.png" width="700" />
## Please share with the community ## Please share with the community
As an open-source project without funding, I cannot afford advertising QuestPDF in a typical way. Instead, the library relies on community interactions. Please consider sharing a post about QuestPDF and the value it provides. It really does help! As an open-source project without funding, I cannot afford advertising QuestPDF in a typical way. Instead, the library relies on community interactions. Please consider sharing a post about QuestPDF and the value it provides. It really does help!