Compare commits

..

19 Commits

Author SHA1 Message Date
MarcinZiabek 5390fc3f1b Implemented font-fallback as required configuration 2022-09-10 23:27:27 +02:00
MarcinZiabek 0468dd5f02 Font-fallback implementation 2022-09-08 13:23:59 +02:00
MarcinZiabek 3ba09ea826 Optimization: do not apply column element when text contains only one paragraph 2022-09-07 13:41:46 +02:00
MarcinZiabek 92abd32aae Updated dependencies 2022-09-07 13:03:30 +02:00
MarcinZiabek e17867d1f3 Added checking if all text glyphs are available in specified font 2022-09-07 00:54:39 +02:00
Marcin Ziąbek bd71f30c78 Merge pull request #326 from Bebo-Maker/fix-documentation-link-in-previewer
Fix documentation link for previewer
2022-09-06 16:09:38 +02:00
MarcinZiabek 6a78a4ebdb Moved document rendering flags to QuestPDF.Settings class 2022-09-06 13:45:48 +02:00
Bennet Bo Fenner 9fc721d66b Fix documentation link for previewer 2022-08-22 13:23:44 +02:00
MarcinZiabek 7d62dead86 2022.8.2 Version type 2022-08-21 20:14:16 +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
30 changed files with 498 additions and 60 deletions
+4 -4
View File
@@ -6,11 +6,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
</ItemGroup>
+39
View File
@@ -2,6 +2,7 @@
using System.Linq;
using System.Text;
using NUnit.Framework;
using QuestPDF.Elements.Text;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
@@ -618,5 +619,43 @@ namespace QuestPDF.Examples
.FontSize(20);
});
}
[Test]
public void FontFallback()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x
.Fallback(y => y.FontFamily("Segoe UI Emoji")
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
page.Size(PageSizes.A4);
page.Content().Text(t =>
{
t.Line("This is normal text.");
t.EmptyLine();
t.Line("Following line should use font fallback:");
t.Line("中文文本");
t.EmptyLine();
t.Line("The following line contains a mix of known and unknown characters.");
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
t.EmptyLine();
t.Line("Emojis work out of the box because of font fallback: 😊😅🥳👍❤😍👌");
});
});
});
}
}
}
@@ -49,7 +49,7 @@ namespace QuestPDF.Previewer
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.html"));
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
}
+2 -2
View File
@@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample
Content = documentContainer.Compose();
PageContext = new PageContext();
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
var sw = new Stopwatch();
sw.Start();
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample
[Benchmark]
public void GenerationTest()
{
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
}
}
}
+2 -3
View File
@@ -48,11 +48,10 @@ namespace QuestPDF.ReportSample
Report.Compose(container);
var content = container.Compose();
var metadata = Report.GetMetadata();
var pageContext = new PageContext();
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.1.0" />
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
+9 -11
View File
@@ -66,19 +66,17 @@ namespace QuestPDF.Drawing
var content = container.Compose();
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
var metadata = document.GetMetadata();
var pageContext = new PageContext();
var debuggingState = metadata.ApplyDebugging ? ApplyDebugging(content) : null;
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
if (metadata.ApplyCaching)
if (Settings.EnableCaching)
ApplyCaching(content);
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
RenderPass(pageContext, canvas, content, metadata, debuggingState);
var pageContext = new PageContext();
RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
RenderPass(pageContext, canvas, content, debuggingState);
}
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas
{
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
@@ -114,7 +112,7 @@ namespace QuestPDF.Drawing
canvas.EndPage();
if (currentPage >= documentMetadata.DocumentLayoutExceptionThreshold)
if (currentPage >= Settings.DocumentLayoutExceptionThreshold)
{
canvas.EndDocument();
ThrowLayoutException();
@@ -131,8 +129,8 @@ namespace QuestPDF.Drawing
void ThrowLayoutException()
{
var message = $"Composed layout generates infinite document. This may happen in two cases. " +
$"1) Your document and its layout configuration is correct but the content takes more than {documentMetadata.DocumentLayoutExceptionThreshold} pages. " +
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
$"1) Your document and its layout configuration is correct but the content takes more than {Settings.DocumentLayoutExceptionThreshold} pages. " +
$"In this case, please increase the value {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.DocumentLayoutExceptionThreshold)} static property. " +
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
$"Please analyze your documents structure to detect this element and fix its size constraints.";
+20 -7
View File
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing
{
@@ -18,14 +19,26 @@ namespace QuestPDF.Drawing
public DateTime CreationDate { get; set; } = DateTime.Now;
public DateTime ModifiedDate { get; set; } = DateTime.Now;
/// <summary>
/// If the number of generated pages exceeds this threshold
/// (likely due to infinite layout), the exception is thrown.
/// </summary>
public int DocumentLayoutExceptionThreshold { get; set; } = 250;
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.DocumentLayoutExceptionThreshold static property.")]
public int DocumentLayoutExceptionThreshold
{
get => Settings.DocumentLayoutExceptionThreshold;
set => Settings.DocumentLayoutExceptionThreshold = value;
}
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableCaching static property.")]
public bool ApplyCaching
{
get => Settings.EnableCaching;
set => Settings.EnableCaching = value;
}
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")]
public bool ApplyDebugging
{
get => Settings.EnableDebugging;
set => Settings.EnableDebugging = value;
}
public static DocumentMetadata Default => new DocumentMetadata();
}
@@ -4,6 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentDrawingException : Exception
{
internal DocumentDrawingException(string message) : base(message)
{
}
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
{
+30 -7
View File
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using HarfBuzzSharp;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -9,14 +10,16 @@ namespace QuestPDF.Drawing
internal class TextShaper
{
public const int FontShapingScale = 512;
private Font Font { get; }
private SKPaint Paint { get; }
public TextShaper(TextStyle style)
private TextStyle TextStyle { get; }
private SKFont Font => TextStyle.ToFont();
private Font ShaperFont => TextStyle.ToShaperFont();
private SKPaint Paint => TextStyle.ToPaint();
public TextShaper(TextStyle textStyle)
{
Font = style.ToShaperFont();
Paint = style.ToPaint();
TextStyle = textStyle;
}
public TextShapingResult Shape(string text)
@@ -26,7 +29,7 @@ namespace QuestPDF.Drawing
PopulateBufferWithText(buffer, text);
buffer.GuessSegmentProperties();
Font.Shape(buffer);
ShaperFont.Shape(buffer);
var length = buffer.Length;
var glyphInfos = buffer.GlyphInfos;
@@ -52,6 +55,9 @@ namespace QuestPDF.Drawing
xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY;
}
if (Settings.CheckIfAllTextGlyphsAreAvailableInSpecifiedFont)
CheckIfAllGlyphsAreAvailable(glyphs, text);
return new TextShapingResult(glyphs);
}
@@ -72,6 +78,20 @@ namespace QuestPDF.Drawing
else
throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
}
void CheckIfAllGlyphsAreAvailable(ShapedGlyph[] glyphs, string originalText)
{
var containsMissingGlyphs = glyphs.Any(x => x.Codepoint == default);
if (!containsMissingGlyphs)
return;
throw new ArgumentException(
$"Detected missing font glyphs while rendering text. " +
$"This means that the document contains text with characters not present in the assigned font. " +
$"Such characters are replaced by placeholders, usually visible as empty rectangles. " +
$"Font family used: {TextStyle.FontFamily}. Issue detected in text: '{originalText}'");
}
}
internal struct ShapedGlyph
@@ -129,6 +149,9 @@ namespace QuestPDF.Drawing
{
if (Glyphs.Length == 0)
return null;
if (startIndex > endIndex)
return null;
using var skTextBlobBuilder = new SKTextBlobBuilder();
+4 -1
View File
@@ -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)
+6 -1
View File
@@ -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)
+4 -1
View File
@@ -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)
+4 -1
View File
@@ -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)
+4 -1
View File
@@ -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)
+3
View File
@@ -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),
+4
View File
@@ -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);
+147
View File
@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Elements.Text
{
internal static class FontFallback
{
public struct TextRun
{
public string Content { get; set; }
public TextStyle Style { get; set; }
}
public class FallbackOption
{
public TextStyle Style { get; set; }
public SKFont Font { get; set; }
public SKTypeface Typeface { get; set; }
}
private static SKFontManager FontManager => SKFontManager.Default;
public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
{
var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
var spanStartIndex = 0;
var spanFallbackOption = fallbackOptions[0];
for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
{
var codepoint = char.ConvertToUtf32(text, i);
var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
if (newFallbackOption == spanFallbackOption)
continue;
yield return new TextRun
{
Content = text.Substring(spanStartIndex, i - spanStartIndex),
Style = spanFallbackOption.Style
};
spanStartIndex = i;
spanFallbackOption = newFallbackOption;
}
if (spanStartIndex > text.Length)
yield break;
yield return new TextRun
{
Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
Style = spanFallbackOption.Style
};
static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
{
while (textStyle != null)
{
var font = textStyle.ToFont();
yield return new FallbackOption
{
Style = textStyle,
Font = font,
Typeface = font.Typeface
};
textStyle = textStyle.Fallback;
}
}
static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
{
foreach (var fallbackOption in fallbackOptions)
{
if (fallbackOption.Font.ContainsGlyph(codepoint))
return fallbackOption;
}
var character = char.ConvertFromUtf32(codepoint);
var unicode = $"U-{codepoint:X4}";
var proposedFonts = FindFontsContainingGlyph(codepoint);
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
throw new DocumentDrawingException(
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
$"Possible solutions: " +
$"1) Use one of the listed fonts as the primary font in your document. " +
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
}
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
{
var fontManager = SKFontManager.Default;
return fontManager
.GetFontFamilies()
.Select(fontManager.MatchFamily)
.Where(x => x.ContainsGlyph(codepoint))
.Select(x => x.FamilyName);
}
}
public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
{
foreach (var textBlockItem in textBlockItems)
{
if (textBlockItem is TextBlockSpan textBlockSpan)
{
// perform font-fallback operation only when any fallback is available
if (textBlockSpan.Style.Fallback == null)
{
yield return textBlockSpan;
continue;
}
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
foreach (var textRun in textRuns)
{
yield return new TextBlockSpan
{
Text = textRun.Content,
Style = textRun.Style
};
}
}
else
{
yield return textBlockItem;
}
}
}
}
}
@@ -15,7 +15,7 @@ namespace QuestPDF.Elements.Text.Items
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new();
public TextShapingResult? TextShapingResult { get; set; }
private TextShapingResult? TextShapingResult { get; set; }
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
protected virtual bool EnableTextCache => true;
@@ -66,7 +66,7 @@ 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;
// break text only on spaces
+12
View File
@@ -18,8 +18,11 @@ namespace QuestPDF.Elements.Text
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
private bool FontFallbackApplied { get; set; } = false;
public void ResetState()
{
ApplyFontFallback();
InitializeQueue();
CurrentElementIndex = 0;
@@ -37,6 +40,15 @@ namespace QuestPDF.Elements.Text
foreach (var item in Items)
RenderingQueue.Enqueue(item);
}
void ApplyFontFallback()
{
if (FontFallbackApplied)
return;
Items = Items.ApplyFontFallback().ToList();
FontFallbackApplied = true;
}
}
internal override SpacePlan Measure(Size availableSpace)
+9 -2
View File
@@ -251,14 +251,21 @@ namespace QuestPDF.Fluent
internal void Compose(IContainer container)
{
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
container = container.DefaultTextStyle(DefaultStyle);
container.DefaultTextStyle(DefaultStyle).Column(column =>
if (TextBlocks.Count == 1)
{
container.Element(TextBlocks.First());
return;
}
container.Column(column =>
{
column.Spacing(Spacing);
foreach (var textBlock in TextBlocks)
column.Item().Element(textBlock);
});
});
}
}
@@ -15,6 +15,17 @@ namespace QuestPDF.Fluent
return descriptor;
}
public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
{
descriptor.TextStyle.Fallback = value;
return descriptor;
}
public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
{
return descriptor.Fallback(handler(TextStyle.Default));
}
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.Color = value;
+10
View File
@@ -14,6 +14,16 @@ namespace QuestPDF.Fluent
return style;
}
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
{
return style.Mutate(x => x.Fallback = value);
}
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
{
return style.Fallback(handler(TextStyle.Default));
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
public static TextStyle Color(this TextStyle style, string value)
{
+6
View File
@@ -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;
}
}
}
+25 -14
View File
@@ -1,4 +1,5 @@
using System;
using HarfBuzzSharp;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
@@ -19,12 +20,12 @@ namespace QuestPDF.Infrastructure
internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; }
internal object PaintKey { get; private set; }
internal object FontMetricsKey { get; private set; }
internal TextStyle? Fallback { get; set; }
// TODO: without cache, this may be an expensive operation
internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic);
// 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)
internal static readonly TextStyle LibraryDefault = new TextStyle
{
Color = Colors.Black,
@@ -37,13 +38,12 @@ namespace QuestPDF.Infrastructure
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false,
WrapAnywhere = false
WrapAnywhere = false,
Fallback = null
};
// 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)
{
@@ -51,13 +51,19 @@ namespace QuestPDF.Infrastructure
return;
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
if (Fallback != null)
ApplyFallbackStyle(this);
}
internal void ApplyFallbackStyle(TextStyle parentStyle)
{
ApplyParentStyle(parentStyle, false);
Fallback?.ApplyFallbackStyle(this);
}
internal void ApplyParentStyle(TextStyle parentStyle)
internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true)
{
Color ??= parentStyle.Color;
BackgroundColor ??= parentStyle.BackgroundColor;
@@ -70,6 +76,9 @@ namespace QuestPDF.Infrastructure
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
if (mapFallback)
Fallback ??= parentStyle.Fallback?.Clone();
}
internal void OverrideStyle(TextStyle parentStyle)
@@ -85,12 +94,14 @@ namespace QuestPDF.Infrastructure
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
Fallback = parentStyle.Fallback?.Clone() ?? Fallback;
}
internal TextStyle Clone()
{
var clone = (TextStyle)MemberwiseClone();
clone.HasGlobalStyleApplied = false;
clone.Fallback = Fallback?.Clone();
return clone;
}
}
+1 -1
View File
@@ -3,7 +3,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.8.0</Version>
<Version>2022.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>
+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,
- 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
2022.8.2
- Fixed: the Column element incorrectly renders zero-height elements.
+40
View File
@@ -0,0 +1,40 @@
namespace QuestPDF
{
public static class Settings
{
/// <summary>
/// This value represents the maximum lenght of the document that the library produces.
/// This is useful when layout constraints are too strong, e.g. one element does not fit in another.
/// In such cases, the library would produce document of infinite length, consuming all available resources.
/// To break the algorithm and save the environment, the library breaks the rendering process after reaching specified length of document.
/// If your content requires generating longer documents, please assign the most reasonable value.
/// </summary>
public static int DocumentLayoutExceptionThreshold { get; set; } = 250;
/// <summary>
/// This flag generates additional document elements to cache layout calculation results.
/// In the vast majority of cases, this significantly improves performance, while slightly increasing memory consumption.
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger is NOT attached.</remarks>
public static bool EnableCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
/// <summary>
/// This flag generates additional document elements to improve layout debugging experience.
/// When the DocumentLayoutException is thrown, the library is able to provide additional execution context.
/// It includes layout calculation results and path to the problematic area.
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
/// <summary>
/// This flag enables checking the font glyph availability.
/// If your text contains glyphs that are not present in the specified font,
/// 1) when this flag is enabled: the DocumentDrawingException is thrown. OR
/// 2) when this flag is disabled: placeholder characters are visible in the produced PDF file.
/// Enabling this flag may slightly decrease document generation performance.
/// However, it provides hints that used fonts are not sufficient to produce correct results.
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
public static bool CheckIfAllTextGlyphsAreAvailableInSpecifiedFont { get; set; } = System.Diagnostics.Debugger.IsAttached;
}
}
+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! ⭐
<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!