Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cbaebdd670 | |||
| 52ad0f5c24 | |||
| 53107073a6 | |||
| c71bb3ea59 | |||
| 6cced3c143 | |||
| 425ea59cfe | |||
| 4d326496c6 | |||
| b7b6488d16 | |||
| f0ba5fc32d | |||
| 04da32e0e7 | |||
| 5bdb338996 |
@@ -129,6 +129,9 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
if (Glyphs.Length == 0)
|
||||
return null;
|
||||
|
||||
if (startIndex > endIndex)
|
||||
return null;
|
||||
|
||||
using var skTextBlobBuilder = new SKTextBlobBuilder();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -12,7 +13,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
|
||||
@@ -94,7 +94,12 @@ namespace QuestPDF.Elements
|
||||
if (item.IsRendered)
|
||||
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);
|
||||
|
||||
if (measurement.Type == SpacePlanType.Wrap)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -11,7 +12,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
@@ -9,7 +10,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -15,7 +16,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
|
||||
@@ -23,6 +23,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
return Type switch
|
||||
{
|
||||
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
@@ -14,6 +15,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
|
||||
@@ -19,5 +19,6 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
public int TotalIndex { get; set; }
|
||||
|
||||
public bool IsLast => EndIndex == TotalIndex;
|
||||
public bool IsNewLine { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -39,18 +39,34 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
var paint = Style.ToPaint();
|
||||
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;
|
||||
|
||||
// if the element is the first one within the line,
|
||||
// ignore leading spaces
|
||||
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++;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
return new TextMeasurementResult
|
||||
@@ -66,8 +82,10 @@ namespace QuestPDF.Elements.Text.Items
|
||||
// start breaking text from requested position
|
||||
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
|
||||
|
||||
if (endIndex < 0)
|
||||
if (endIndex < startIndex)
|
||||
return null;
|
||||
|
||||
endIndex = Math.Min(endIndex, newLineIndex);
|
||||
|
||||
// break text only on spaces
|
||||
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
|
||||
@@ -90,7 +108,9 @@ namespace QuestPDF.Elements.Text.Items
|
||||
StartIndex = startIndex,
|
||||
EndIndex = wrappedText.Value.endIndex,
|
||||
NextIndex = wrappedText.Value.nextIndex,
|
||||
TotalIndex = TextShapingResult.Glyphs.Length - 1
|
||||
TotalIndex = TextShapingResult.Glyphs.Length - 1,
|
||||
|
||||
IsNewLine = endIndex == newLineIndex
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ 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 float ParagraphSpacing { get; set; } = 0;
|
||||
|
||||
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));
|
||||
|
||||
heightOffset += line.LineHeight;
|
||||
|
||||
if (line.Elements.Last().Measurement.IsNewLine)
|
||||
{
|
||||
heightOffset += ParagraphSpacing;
|
||||
Canvas.Translate(new Position(0, ParagraphSpacing));
|
||||
}
|
||||
}
|
||||
|
||||
Canvas.Translate(new Position(0, -heightOffset));
|
||||
|
||||
@@ -40,10 +40,9 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public class TextDescriptor
|
||||
{
|
||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||
internal TextBlock TextBlock = new();
|
||||
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
|
||||
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
private float Spacing { get; set; } = 0f;
|
||||
|
||||
public void DefaultTextStyle(TextStyle style)
|
||||
{
|
||||
@@ -72,17 +71,9 @@ namespace QuestPDF.Fluent
|
||||
|
||||
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.")]
|
||||
public void Span(string? text, TextStyle style)
|
||||
{
|
||||
@@ -96,28 +87,13 @@ namespace QuestPDF.Fluent
|
||||
|
||||
if (text == null)
|
||||
return descriptor;
|
||||
|
||||
var items = text
|
||||
.Replace("\r", string.Empty)
|
||||
.Split(new[] { '\n' }, StringSplitOptions.None)
|
||||
.Select(x => new TextBlockSpan
|
||||
{
|
||||
Text = x,
|
||||
Style = style
|
||||
})
|
||||
.ToList();
|
||||
|
||||
AddItemToLastTextBlock(items.First());
|
||||
|
||||
items
|
||||
.Skip(1)
|
||||
.Select(x => new TextBlock
|
||||
{
|
||||
Items = new List<ITextBlockItem> { x }
|
||||
})
|
||||
.ToList()
|
||||
.ForEach(TextBlocks.Add);
|
||||
|
||||
|
||||
TextBlock.Items.Add(new TextBlockSpan
|
||||
{
|
||||
Text = text,
|
||||
Style = style
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@@ -137,7 +113,7 @@ namespace QuestPDF.Fluent
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextPageNumberDescriptor(style);
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockPageNumber
|
||||
TextBlock.Items.Add(new TextBlockPageNumber
|
||||
{
|
||||
Source = context => descriptor.FormatFunction(pageNumber(context)),
|
||||
Style = style
|
||||
@@ -193,7 +169,7 @@ namespace QuestPDF.Fluent
|
||||
if (IsNullOrEmpty(text))
|
||||
return descriptor;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockSectionLink
|
||||
TextBlock.Items.Add(new TextBlockSectionLink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
@@ -220,7 +196,7 @@ namespace QuestPDF.Fluent
|
||||
if (IsNullOrEmpty(text))
|
||||
return descriptor;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
||||
TextBlock.Items.Add(new TextBlockHyperlink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
@@ -240,26 +216,13 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
var container = new Container();
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockElement
|
||||
TextBlock.Items.Add(new TextBlockElement
|
||||
{
|
||||
Element = container
|
||||
});
|
||||
|
||||
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
|
||||
@@ -272,7 +235,7 @@ namespace QuestPDF.Fluent
|
||||
descriptor.Alignment = alignment.Horizontal;
|
||||
|
||||
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.")]
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Helpers
|
||||
@@ -54,5 +55,10 @@ namespace QuestPDF.Helpers
|
||||
|
||||
handler(element);
|
||||
}
|
||||
|
||||
internal static bool IsNegative(this Size size)
|
||||
{
|
||||
return size.Width < 0f || size.Height < 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,8 @@ namespace QuestPDF.Infrastructure
|
||||
WrapAnywhere = false
|
||||
};
|
||||
|
||||
// REVIEW: Should this be a method call that news up a TextStyle,
|
||||
// or can it be a static variable?
|
||||
// (style mutations seem to create a clone anyway)
|
||||
public static readonly TextStyle Default = new TextStyle();
|
||||
// it is important to create new instances for the DefaultTextStyle element to work correctly
|
||||
public static TextStyle Default => new TextStyle();
|
||||
|
||||
internal void ApplyGlobalStyle(TextStyle globalStyle)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<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>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>9</LangVersion>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](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 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
|
||||
|
||||
[](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.
|
||||
|
||||
|
||||
[](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.
|
||||
|
||||
|
||||
[](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:
|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
|
||||

|
||||
@@ -1,3 +1,5 @@
|
||||
2022.8.0:
|
||||
|
||||
- Improved library performance,
|
||||
- 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,
|
||||
@@ -5,3 +7,14 @@
|
||||
- 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 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.
|
||||
|
||||
@@ -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! ⭐
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/9263853/184642026-27dd7567-a46a-45d4-9594-e6a70a7193e9.png" width="700" />
|
||||
|
||||
## 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!
|
||||
|
||||
Reference in New Issue
Block a user