Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e53b586943 | |||
| 9637dff1dc | |||
| 1c9bccbeb2 |
@@ -54,7 +54,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/232912983-b20e441e-bd47-41df-afa4-f02481252d26.png" width="800" />
|
||||
<img src="https://user-images.githubusercontent.com/9263853/224511996-9dd27a99-bef8-4d5e-bc14-9f6cfe0f5191.png" width="800" />
|
||||
|
||||
|
||||
## Please share with the community
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
@@ -64,42 +66,24 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Test]
|
||||
public void ReusingTheSameImageFileShouldBePossible()
|
||||
public void ImageResolutionScaling()
|
||||
{
|
||||
var fileName = Path.GetTempFileName() + ".jpg";
|
||||
var image = Image.FromFile("large-image.jpg");
|
||||
|
||||
try
|
||||
{
|
||||
var image = Placeholders.Image(300, 100);
|
||||
|
||||
using var file = File.Create(fileName);
|
||||
file.Write(image);
|
||||
file.Dispose();
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProducePdf()
|
||||
.PageSize(PageSizes.A4)
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
Document
|
||||
.Create(document =>
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
container
|
||||
.Padding(20)
|
||||
.Column(column =>
|
||||
{
|
||||
column.Spacing(20);
|
||||
|
||||
column.Item().Image(fileName);
|
||||
column.Item().Image(fileName);
|
||||
column.Item().Image(fileName);
|
||||
});
|
||||
page.Size(210, 210);
|
||||
page.Margin(50);
|
||||
page.Content().Image(image);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(fileName);
|
||||
}
|
||||
})
|
||||
.GeneratePdf($"test.pdf");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
<None Update="pdf-icon.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="large-image.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="large-image.jpg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -316,58 +316,6 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Bug_RowSpanWorksIncorrectly()
|
||||
{
|
||||
// https://github.com/QuestPDF/QuestPDF/issues/552
|
||||
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.PageSize(PageSizes.A5.Landscape())
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container.Padding(20).Table(table =>
|
||||
{
|
||||
table.ColumnsDefinition(columns =>
|
||||
{
|
||||
columns.RelativeColumn(1);
|
||||
columns.RelativeColumn(2);
|
||||
columns.RelativeColumn(2);
|
||||
columns.RelativeColumn(2);
|
||||
});
|
||||
|
||||
foreach (var i in Enumerable.Range(6, 9))
|
||||
{
|
||||
table.Cell().Element(CellStyleMainTable).Text($"{i:00}:00");
|
||||
|
||||
foreach (var j in Enumerable.Range(1, 3))
|
||||
table.Cell().Element(CellStyleMainTable).Text("");
|
||||
}
|
||||
|
||||
static IContainer CellStyleMainTable(IContainer container)
|
||||
{
|
||||
return container
|
||||
.Border(0.5f).BorderColor(Colors.Blue.Lighten4)
|
||||
.Background(Colors.Blue.Lighten5)
|
||||
.PaddingVertical(5);
|
||||
}
|
||||
|
||||
table.Cell().Row(1).RowSpan(3).Column(2).Element(BlockAccepted).Text("3 rows");
|
||||
table.Cell().Row(1).RowSpan(6).Column(3).Element(BlockAccepted).Text("6 rows");
|
||||
table.Cell().Row(3).RowSpan(5).Column(4).Element(BlockAccepted).Text("5 rows");
|
||||
|
||||
static IContainer BlockAccepted(IContainer container)
|
||||
{
|
||||
return container
|
||||
.Border(1.5f).BorderColor(Colors.Green.Lighten2)
|
||||
.Background(Colors.Green.Lighten4);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TableHeader()
|
||||
{
|
||||
|
||||
@@ -1004,40 +1004,5 @@ namespace QuestPDF.Examples
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontFallback_Nested()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.PageColor(Colors.White);
|
||||
page.Size(PageSizes.A5.Landscape());
|
||||
|
||||
page.DefaultTextStyle(x => x
|
||||
.FontSize(24)
|
||||
.Bold()
|
||||
.FontFamily("Times New Roman")
|
||||
.Fallback(y => y
|
||||
.FontFamily("Microsoft YaHei")
|
||||
.Underline()
|
||||
.BackgroundColor(Colors.Red.Lighten2)));
|
||||
|
||||
page.Content().Text(text =>
|
||||
{
|
||||
text.Line("Default times new roman 中文文本 text.");
|
||||
text.Line("Normal weight and green 中文文本 text.").NormalWeight().BackgroundColor(Colors.Green.Lighten2);
|
||||
text.Line("Strikethrough without underline 中文文本 text.").Strikethrough().Underline(false);
|
||||
text.Line("Lato italic 中文文本 text.").FontFamily("Lato").Italic();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
@@ -1,10 +1,16 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using QuestPDF.UnitTests.TestEngine;
|
||||
using SkiaSharp;
|
||||
using Image = QuestPDF.Infrastructure.Image;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
@@ -15,9 +21,9 @@ namespace QuestPDF.UnitTests
|
||||
public void Measure_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
.For(x => new QuestPDF.Elements.Image
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
DocumentImage = Image.FromSkImage(GenerateImage(400, 300))
|
||||
})
|
||||
.MeasureElement(new Size(300, 200))
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 200));
|
||||
@@ -27,9 +33,9 @@ namespace QuestPDF.UnitTests
|
||||
public void Draw_TakesAvailableSpaceRegardlessOfSize()
|
||||
{
|
||||
TestPlan
|
||||
.For(x => new Image
|
||||
.For(x => new QuestPDF.Elements.Image
|
||||
{
|
||||
InternalImage = GenerateImage(400, 300)
|
||||
DocumentImage = Image.FromSkImage(GenerateImage(400, 300))
|
||||
})
|
||||
.DrawElement(new Size(300, 200))
|
||||
.ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
|
||||
@@ -51,6 +57,52 @@ namespace QuestPDF.UnitTests
|
||||
.MeasureElement(new Size(300, 200))
|
||||
.CheckMeasureResult(SpacePlan.FullRender(300, 100));;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize()
|
||||
{
|
||||
var placeholderImage = Placeholders.Image(1000, 200);
|
||||
|
||||
var documentWithSingleImageSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Image(placeholderImage);
|
||||
});
|
||||
|
||||
var documentWithMultipleImagesSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Column(column =>
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, 100))
|
||||
column.Item().Image(placeholderImage);
|
||||
});
|
||||
});
|
||||
|
||||
var documentWithSingleImageUsedMultipleTimesSize = GetDocumentSize(container =>
|
||||
{
|
||||
container.Column(column =>
|
||||
{
|
||||
var sharedImage = Image.FromBinaryData(placeholderImage).DisposeAfterDocumentGeneration();
|
||||
|
||||
foreach (var i in Enumerable.Range(0, 100))
|
||||
column.Item().Image(sharedImage);
|
||||
});
|
||||
});
|
||||
|
||||
(documentWithMultipleImagesSize / (float)documentWithSingleImageSize).Should().BeInRange(90, 100);
|
||||
(documentWithSingleImageUsedMultipleTimesSize / (float)documentWithSingleImageSize).Should().BeInRange(1f, 1.5f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ImageShouldNotBeScaledAboveItsNativeResolution()
|
||||
{
|
||||
var image = Placeholders.Image(200, 200);
|
||||
|
||||
var documentSizeWithScaledDownImage = GetDocumentSize(container => container.Width(100).Height(100).Image(Image.FromBinaryData(image)));
|
||||
//var documentSizeWithNormalImage = GetDocumentSize(container => container.Width(200).Height(200).Image(image));
|
||||
//var documentSizeWithScaledUpImage = GetDocumentSize(container => container.Width(400).Height(400).Image(image));
|
||||
}
|
||||
|
||||
#region helpers
|
||||
|
||||
SKImage GenerateImage(int width, int height)
|
||||
{
|
||||
@@ -58,5 +110,21 @@ namespace QuestPDF.UnitTests
|
||||
using var surface = SKSurface.Create(imageInfo);
|
||||
return surface.Snapshot();
|
||||
}
|
||||
|
||||
private static int GetDocumentSize(Action<IContainer> container)
|
||||
{
|
||||
return Document
|
||||
.Create(document =>
|
||||
{
|
||||
document.Page(page =>
|
||||
{
|
||||
page.Content().Element(container);
|
||||
});
|
||||
})
|
||||
.GeneratePdf()
|
||||
.Length;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<LangVersion>9</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TextStyleTests
|
||||
{
|
||||
[Test]
|
||||
public void ApplyInheritedAndGlobalStyle()
|
||||
{
|
||||
// arrange
|
||||
var defaultTextStyle = TextStyle
|
||||
.Default
|
||||
.FontSize(20)
|
||||
.FontFamily("Arial")
|
||||
.BackgroundColor(Colors.Green.Lighten2)
|
||||
.Fallback(y => y
|
||||
.FontFamily("Microsoft YaHei")
|
||||
.Underline()
|
||||
.NormalWeight()
|
||||
.BackgroundColor(Colors.Blue.Lighten2));
|
||||
|
||||
var spanTextStyle = TextStyle
|
||||
.Default
|
||||
.FontFamily("Times New Roman")
|
||||
.Bold()
|
||||
.Strikethrough()
|
||||
.BackgroundColor(Colors.Red.Lighten2);
|
||||
|
||||
// act
|
||||
var targetStyle = spanTextStyle.ApplyInheritedStyle(defaultTextStyle).ApplyGlobalStyle();
|
||||
|
||||
// assert
|
||||
var expectedStyle = TextStyle.LibraryDefault with
|
||||
{
|
||||
Size = 20,
|
||||
FontFamily = "Times New Roman",
|
||||
FontWeight = FontWeight.Bold,
|
||||
BackgroundColor = Colors.Red.Lighten2,
|
||||
HasStrikethrough = true,
|
||||
Fallback = TextStyle.LibraryDefault with
|
||||
{
|
||||
Size = 20,
|
||||
FontFamily = "Microsoft YaHei",
|
||||
FontWeight = FontWeight.Bold,
|
||||
BackgroundColor = Colors.Red.Lighten2,
|
||||
HasUnderline = true,
|
||||
HasStrikethrough = true
|
||||
}
|
||||
};
|
||||
|
||||
targetStyle.Should().BeEquivalentTo(expectedStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using QuestPDF.Elements.Text.Items;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using Image = QuestPDF.Elements.Image;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
@@ -21,7 +22,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new PdfCanvas(stream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
RenderDocument(canvas, document, metadata);
|
||||
}
|
||||
|
||||
internal static void GenerateXps(Stream stream, IDocument document)
|
||||
@@ -30,7 +31,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new XpsCanvas(stream, metadata);
|
||||
RenderDocument(canvas, document);
|
||||
RenderDocument(canvas, document, metadata);
|
||||
}
|
||||
|
||||
private static void CheckIfStreamIsCompatible(Stream stream)
|
||||
@@ -46,25 +47,27 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new ImageCanvas(metadata);
|
||||
RenderDocument(canvas, document);
|
||||
RenderDocument(canvas, document, metadata);
|
||||
|
||||
return canvas.Images;
|
||||
}
|
||||
|
||||
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
|
||||
{
|
||||
var metadata = document.GetMetadata();
|
||||
var canvas = new SkiaPictureCanvas();
|
||||
RenderDocument(canvas, document);
|
||||
RenderDocument(canvas, document, metadata);
|
||||
return canvas.Pictures;
|
||||
}
|
||||
|
||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentMetadata metadata)
|
||||
where TCanvas : ICanvas, IRenderingCanvas
|
||||
{
|
||||
var container = new DocumentContainer();
|
||||
document.Compose(container);
|
||||
var content = container.Compose();
|
||||
ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
|
||||
ApplyDefaultImageDpi(content, metadata.RasterDpi, metadata.ImageQuality);
|
||||
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
|
||||
ApplyContentDirection(content, ContentDirection.LeftToRight);
|
||||
|
||||
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
|
||||
@@ -173,6 +176,18 @@ namespace QuestPDF.Drawing
|
||||
|
||||
return debuggingState;
|
||||
}
|
||||
|
||||
internal static void ApplyDefaultImageDpi(this Element? content, int targetDpi, int targetImageQuality)
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
if (x is not Image { DocumentImage: { } image })
|
||||
return;
|
||||
|
||||
image.TargetDpi ??= targetDpi;
|
||||
image.ImageQuality ??= targetImageQuality;
|
||||
});
|
||||
}
|
||||
|
||||
internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
|
||||
{
|
||||
@@ -192,7 +207,7 @@ namespace QuestPDF.Drawing
|
||||
ApplyContentDirection(child, direction);
|
||||
}
|
||||
|
||||
internal static void ApplyInheritedAndGlobalTexStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
||||
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
||||
{
|
||||
if (content == null)
|
||||
return;
|
||||
@@ -202,23 +217,26 @@ namespace QuestPDF.Drawing
|
||||
foreach (var textBlockItem in textBlock.Items)
|
||||
{
|
||||
if (textBlockItem is TextBlockSpan textSpan)
|
||||
textSpan.Style = textSpan.Style.ApplyInheritedStyle(documentDefaultTextStyle).ApplyGlobalStyle();
|
||||
|
||||
if (textBlockItem is TextBlockElement textElement)
|
||||
ApplyInheritedAndGlobalTexStyle(textElement.Element, documentDefaultTextStyle);
|
||||
{
|
||||
textSpan.Style = textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
}
|
||||
else if (textBlockItem is TextBlockElement textElement)
|
||||
{
|
||||
ApplyDefaultTextStyle(textElement.Element, documentDefaultTextStyle);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (content is DynamicHost dynamicHost)
|
||||
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyInheritedStyle(documentDefaultTextStyle);
|
||||
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyInheritedStyle(documentDefaultTextStyle);
|
||||
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
foreach (var child in content.GetChildren())
|
||||
ApplyInheritedAndGlobalTexStyle(child, documentDefaultTextStyle);
|
||||
ApplyDefaultTextStyle(child, documentDefaultTextStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
public class DocumentMetadata
|
||||
{
|
||||
public const int DefaultPdfDpi = 72;
|
||||
|
||||
public int ImageQuality { get; set; } = 101;
|
||||
public int RasterDpi { get; set; } = 72;
|
||||
public int RasterDpi { get; set; } = DefaultPdfDpi;
|
||||
public bool PdfA { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
@@ -89,7 +89,7 @@ namespace QuestPDF.Elements
|
||||
var container = new DynamicElement();
|
||||
content(container);
|
||||
|
||||
container.ApplyInheritedAndGlobalTexStyle(TextStyle);
|
||||
container.ApplyDefaultTextStyle(TextStyle);
|
||||
container.ApplyContentDirection(ContentDirection);
|
||||
|
||||
container.InjectDependencies(PageContext, Canvas);
|
||||
|
||||
@@ -7,11 +7,12 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
internal class Image : Element, ICacheable
|
||||
{
|
||||
public SKImage? InternalImage { get; set; }
|
||||
public Infrastructure.Image? DocumentImage { get; set; }
|
||||
|
||||
~Image()
|
||||
{
|
||||
InternalImage?.Dispose();
|
||||
if (DocumentImage is { IsDocumentScoped: true })
|
||||
DocumentImage?.Dispose();
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
@@ -23,10 +24,10 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
if (InternalImage == null)
|
||||
if (DocumentImage == null)
|
||||
return;
|
||||
|
||||
Canvas.DrawImage(InternalImage, Position.Zero, availableSpace);
|
||||
Canvas.DrawImage(DocumentImage.GetVersionOfSize(availableSpace), Position.Zero, availableSpace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ namespace QuestPDF.Elements.Table
|
||||
UpdateColumnsWidth(availableSpace.Width);
|
||||
var renderingCommands = PlanLayout(availableSpace);
|
||||
|
||||
foreach (var command in renderingCommands.OrderBy(x => x.Cell.ZIndex))
|
||||
foreach (var command in renderingCommands)
|
||||
{
|
||||
if (command.Measurement.Type == SpacePlanType.FullRender)
|
||||
command.Cell.IsRendered = true;
|
||||
|
||||
@@ -8,8 +8,6 @@ namespace QuestPDF.Elements.Table
|
||||
public int Column { get; set; } = 0;
|
||||
public int ColumnSpan { get; set; } = 1;
|
||||
|
||||
public int ZIndex { get; set; }
|
||||
|
||||
public bool IsRendered { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,9 @@ namespace QuestPDF.Elements.Table
|
||||
{
|
||||
var cellsWindow = new List<TableCell>();
|
||||
(int x, int y) currentLocation = (1, 1);
|
||||
var zIndex = 0;
|
||||
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
cell.ZIndex = zIndex;
|
||||
zIndex++;
|
||||
|
||||
if (cellsWindow.Count > Math.Max(columnsCount, 16))
|
||||
{
|
||||
cellsWindow = cellsWindow
|
||||
|
||||
@@ -105,9 +105,7 @@ namespace QuestPDF.Elements.Text
|
||||
$"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. " +
|
||||
$"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. " +
|
||||
$"However, this may result with text glyphs being incorrectly rendered without any warning.");
|
||||
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
|
||||
}
|
||||
|
||||
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
@@ -11,30 +10,30 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(imageData);
|
||||
var image = Infrastructure.Image.FromBinaryData(imageData).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(filePath);
|
||||
var image = Infrastructure.Image.FromFile(filePath).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
var image = SKImage.FromEncodedData(fileStream);
|
||||
var image = Infrastructure.Image.FromStream(fileStream).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
}
|
||||
|
||||
private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static void Image(this IContainer parent, Infrastructure.Image image, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
|
||||
var imageElement = new Image
|
||||
var imageElement = new QuestPDF.Elements.Image
|
||||
{
|
||||
InternalImage = image
|
||||
DocumentImage = image
|
||||
};
|
||||
|
||||
if (scaling != ImageScaling.Resize)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public class Image : IDisposable
|
||||
{
|
||||
private SKImage SkImage { get; }
|
||||
internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
|
||||
|
||||
internal int? TargetDpi { get; set; }
|
||||
internal int? ImageQuality { get; set; }
|
||||
internal bool PerformScalingToTargetDpi { get; set; }
|
||||
internal bool IsDocumentScoped { get; set; }
|
||||
|
||||
public int Width => SkImage.Width;
|
||||
public int Height => SkImage.Height;
|
||||
|
||||
private const float ImageSizeSimilarityToleranceMax = 1.1f;
|
||||
private const float ImageSizeSimilarityToleranceMin = 1 / ImageSizeSimilarityToleranceMax;
|
||||
|
||||
private Image(SKImage image)
|
||||
{
|
||||
SkImage = image;
|
||||
PerformScalingToTargetDpi = image.EncodedData.Size >= Settings.AdjustImageSizeThreshold;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SkImage.Dispose();
|
||||
ScaledImageCache?.ForEach(x => x.image.Dispose());
|
||||
}
|
||||
|
||||
internal SKImage GetVersionOfSize(Size size)
|
||||
{
|
||||
if (!PerformScalingToTargetDpi)
|
||||
return SkImage;
|
||||
|
||||
var scalingFactor = TargetDpi.Value / (float)DocumentMetadata.DefaultPdfDpi;
|
||||
var targetResolution = new Size(size.Width * scalingFactor, size.Height * scalingFactor);
|
||||
|
||||
if (targetResolution.Width > Width || targetResolution.Height > Height)
|
||||
return SkImage;
|
||||
|
||||
ScaledImageCache ??= new List<(Size size, SKImage image)>();
|
||||
|
||||
foreach (var imageCache in ScaledImageCache)
|
||||
{
|
||||
if (HasSimilarSize(imageCache.size, targetResolution))
|
||||
return imageCache.image;
|
||||
}
|
||||
|
||||
var scaledImage = ScaleImage(SkImage, targetResolution, ImageQuality);
|
||||
ScaledImageCache.Add((targetResolution, scaledImage));
|
||||
|
||||
if (SkImage.EncodedData.Size < scaledImage.EncodedData.Size)
|
||||
return SkImage;
|
||||
|
||||
return scaledImage;
|
||||
|
||||
|
||||
|
||||
static SKImage ScaleImage(SKImage originalImage, Size targetSize, int? imageQuality)
|
||||
{
|
||||
var imageInfo = new SKImageInfo((int)targetSize.Width, (int)targetSize.Height);
|
||||
using var target = SKImage.Create(imageInfo);
|
||||
originalImage.ScalePixels(target.PeekPixels(), SKFilterQuality.High);
|
||||
|
||||
var codes = SKCodec.Create(target.EncodedData);
|
||||
|
||||
var targetFormat = imageQuality > 100 ? SKEncodedImageFormat.Png : SKEncodedImageFormat.Jpeg;
|
||||
var targetQuality = Math.Max(imageQuality, 100);
|
||||
var data = target.Encode(targetFormat, targetQuality);
|
||||
|
||||
return SKImage.FromEncodedData(data);
|
||||
}
|
||||
|
||||
static (SKEncodedImageFormat format, int quality) GetTargetImageFormat(SKImage originalImage, int? imageQuality)
|
||||
{
|
||||
if (imageQuality.HasValue)
|
||||
{
|
||||
var format = imageQuality > 100
|
||||
? SKEncodedImageFormat.Png
|
||||
: SKEncodedImageFormat.Jpeg;
|
||||
|
||||
var quality = Math.Max(imageQuality.Value, 100);
|
||||
|
||||
return (format, quality);
|
||||
}
|
||||
|
||||
var codec = SKCodec.Create(originalImage.EncodedData);
|
||||
|
||||
}
|
||||
|
||||
static bool HasSimilarSize(Size a, Size b)
|
||||
{
|
||||
var widthRatio = a.Width / b.Width;
|
||||
var heightRatio = a.Height / b.Height;
|
||||
|
||||
return widthRatio is > ImageSizeSimilarityToleranceMin and < ImageSizeSimilarityToleranceMax &&
|
||||
heightRatio is > ImageSizeSimilarityToleranceMin and < ImageSizeSimilarityToleranceMax;
|
||||
}
|
||||
}
|
||||
|
||||
#region public constructors
|
||||
|
||||
internal static Image FromSkImage(SKImage image)
|
||||
{
|
||||
return CreateImage(image);
|
||||
}
|
||||
|
||||
public static Image FromBinaryData(byte[] imageData)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(imageData));
|
||||
}
|
||||
|
||||
public static Image FromFile(string filePath)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(filePath));
|
||||
}
|
||||
|
||||
public static Image FromStream(Stream fileStream)
|
||||
{
|
||||
return CreateImage(SKImage.FromEncodedData(fileStream));
|
||||
}
|
||||
|
||||
private static Image CreateImage(SKImage? image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
|
||||
return new Image(image);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region configuration API
|
||||
|
||||
public Image DisposeAfterDocumentGeneration()
|
||||
{
|
||||
IsDocumentScoped = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values from 1 to 100 correspond to the JPEG format, where 1 is lowest and 100 is highest quality.
|
||||
/// Value 101 correspond to the PNG format with a lossless compression and alpha channel support.
|
||||
/// </summary>
|
||||
/// <param name="quality"></param>
|
||||
/// <returns></returns>
|
||||
public Image WithQuality(int quality)
|
||||
{
|
||||
ImageQuality = quality;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Image WithQuality(ImageQuality quality)
|
||||
{
|
||||
ImageQuality = (int)quality;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Image WithTargetDpi(int dpi = DocumentMetadata.DefaultPdfDpi)
|
||||
{
|
||||
TargetDpi = dpi;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Image ScaleToTargetDpi(bool value = true)
|
||||
{
|
||||
PerformScalingToTargetDpi = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public enum ImageQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// PNG format with alpha support
|
||||
/// </summary>
|
||||
Lossless = 101,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 100 out of 100
|
||||
/// </summary>
|
||||
Max = 100,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 90 out of 100
|
||||
/// </summary>
|
||||
VeryHigh = 90,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 80 out of 100
|
||||
/// </summary>
|
||||
High = 80,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 60 out of 100
|
||||
/// </summary>
|
||||
Medium = 60,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 40 out of 100
|
||||
/// </summary>
|
||||
Low = 40,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 20 out of 100
|
||||
/// </summary>
|
||||
VeryLow = 20
|
||||
}
|
||||
}
|
||||
@@ -25,17 +25,16 @@ namespace QuestPDF.Infrastructure
|
||||
internal static class TextStyleManager
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyInheritedCache = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, TextStyle> TextStyleApplyGlobalCache = new();
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyGlobalCache = new();
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new();
|
||||
|
||||
public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
|
||||
{
|
||||
var cacheKey = (origin, property, value);
|
||||
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value, overrideValue: true));
|
||||
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value));
|
||||
}
|
||||
|
||||
private static TextStyle MutateStyle(this TextStyle origin, TextStyleProperty property, object? value, bool overrideValue)
|
||||
private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object? value, bool overrideValue = true)
|
||||
{
|
||||
if (overrideValue && value == null)
|
||||
return origin;
|
||||
@@ -225,42 +224,40 @@ namespace QuestPDF.Infrastructure
|
||||
throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
|
||||
}
|
||||
|
||||
internal static TextStyle ApplyInheritedStyle(this TextStyle style, TextStyle parent)
|
||||
internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent);
|
||||
return TextStyleApplyInheritedCache.GetOrAdd(cacheKey, key => key.origin.ApplyStyleProperties(key.parent, overrideStyle: false, overrideFontFamily: false, applyFallback: true).UpdateFontFallback(overrideStyle: true));
|
||||
return TextStyleApplyGlobalCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, overrideStyle: false).ApplyFontFallback());
|
||||
}
|
||||
|
||||
internal static TextStyle ApplyGlobalStyle(this TextStyle style)
|
||||
{
|
||||
return TextStyleApplyGlobalCache.GetOrAdd(style, key => key.ApplyStyleProperties(TextStyle.LibraryDefault, overrideStyle: false, overrideFontFamily: false, applyFallback: true).UpdateFontFallback(overrideStyle: false));
|
||||
}
|
||||
|
||||
private static TextStyle UpdateFontFallback(this TextStyle style, bool overrideStyle)
|
||||
private static TextStyle ApplyFontFallback(this TextStyle style)
|
||||
{
|
||||
var targetFallbackStyle = style
|
||||
?.Fallback
|
||||
?.ApplyStyleProperties(style, overrideStyle: overrideStyle, overrideFontFamily: false, applyFallback: false)
|
||||
?.UpdateFontFallback(overrideStyle);
|
||||
?.ApplyStyle(style, overrideStyle: false, applyFallback: false)
|
||||
?.ApplyFontFallback();
|
||||
|
||||
return style.MutateStyle(TextStyleProperty.Fallback, targetFallbackStyle, overrideValue: true);
|
||||
return MutateStyle(style, TextStyleProperty.Fallback, targetFallbackStyle);
|
||||
}
|
||||
|
||||
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent);
|
||||
return TextStyleOverrideCache.GetOrAdd(cacheKey, key => ApplyStyleProperties(key.origin, key.parent, overrideStyle: true, overrideFontFamily: true, applyFallback: true));
|
||||
|
||||
return TextStyleOverrideCache.GetOrAdd(cacheKey, key =>
|
||||
{
|
||||
var result = ApplyStyle(key.origin, key.parent);
|
||||
return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback);
|
||||
});
|
||||
}
|
||||
|
||||
private static TextStyle ApplyStyleProperties(this TextStyle style, TextStyle parent, bool overrideStyle, bool overrideFontFamily, bool applyFallback)
|
||||
private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true)
|
||||
{
|
||||
var result = style;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(result.FontFamily) || overrideFontFamily)
|
||||
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
|
||||
|
||||
|
||||
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.LetterSpacing, parent.LetterSpacing, overrideStyle);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2022.12.5</Version>
|
||||
<Version>2022.12.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. Easily generate PDF reports, invoices, exports, etc.</PackageDescription>
|
||||
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
|
||||
<LangVersion>9</LangVersion>
|
||||
|
||||
@@ -1,27 +1,22 @@
|
||||
Version 2022.12.0:
|
||||
Feature: implemented LetterSpacing property for the Text element
|
||||
Improvement: the Text element API accepts now only string values, objects are not automatically converted anymore
|
||||
Fix: the Alignment element incorrectly limits size of its child when only one axis is set (horizontal or vertical)
|
||||
Maintenance: Updated SkiaSharp dependency to 2.88.3
|
||||
|
||||
This release was possible thanks to enormous help of AntonyCorbett, thank you!
|
||||
|
||||
Version 2022.12.1:
|
||||
In version 2022.12.1:
|
||||
Fixed: loading fonts from embedded resource via the FontManager.RegisterFontFromEmbeddedResource method
|
||||
Fixed: better layout calculation stability for the Column element
|
||||
Improvement: exposed missing API method for the Dynamic component, enabling applying more advanced optimizations
|
||||
Improvement: better API documentation for the Settings.DocumentLayoutExceptionThreshold property
|
||||
|
||||
Version 2022.12.2:
|
||||
In version 2022.12.2:
|
||||
Performance improvements in various areas
|
||||
Text rendering stability improvements
|
||||
Fixed: the Settings.CheckIfAllTextGlyphsAreAvailable setting does not work correctly
|
||||
|
||||
Version 2022.12.3:
|
||||
In version 2022.12.3:
|
||||
Fix: inconsistent text height when using multiple lines with different TextStyles
|
||||
Improvement: added validation for color arguments
|
||||
Fix: the inlined element is shown only once in the header but should be repeated on each page
|
||||
|
||||
Version 2022.12.4:
|
||||
Fix: the TextStyle.Fallback property incorrectly inherits parent's and global properties
|
||||
Improvement: updated the CreateNotMatchingFontException message to mention that the glyph checking operation can be disabled with the Settings.CheckIfAllTextGlyphsAreAvailable setting
|
||||
This release was possible thanks to enormous help of AntonyCorbett, thank you!
|
||||
|
||||
@@ -36,5 +36,11 @@
|
||||
/// </summary>
|
||||
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
|
||||
public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The file size threshold in bytes that is used to determine if the image should be automatically scaled to physical dimensions.
|
||||
/// </summary>
|
||||
public static int AdjustImageSizeThreshold { get; set; } = 32 * 1024;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user