diff --git a/Source/QuestPDF.Examples/ImageExamples.cs b/Source/QuestPDF.Examples/ImageExamples.cs
index 27c97ca..bf9043e 100644
--- a/Source/QuestPDF.Examples/ImageExamples.cs
+++ b/Source/QuestPDF.Examples/ImageExamples.cs
@@ -66,28 +66,24 @@ namespace QuestPDF.Examples
});
}
+
+
[Test]
- public void ReusingTheSameImageFileShouldBePossible()
+ public void ImageResolutionScaling()
{
- var image = Image.FromBinaryData(Placeholders.Image(300, 100)).DisposeAfterDocumentGeneration();
-
- RenderingTest
- .Create()
- .ProducePdf()
- .PageSize(PageSizes.A4)
- .ShowResults()
- .Render(container =>
+ var image = Image.FromFile("large-image.jpg");
+
+ Document
+ .Create(document =>
{
- container
- .Padding(20)
- .Column(column =>
- {
- column.Spacing(20);
-
- foreach (var i in Enumerable.Range(0, 1000))
- column.Item().Image(image);
- });
- });
+ document.Page(page =>
+ {
+ page.Size(210, 210);
+ page.Margin(50);
+ page.Content().Image(image);
+ });
+ })
+ .GeneratePdf($"test.pdf");
}
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
index a053de7..647b4d1 100644
--- a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
+++ b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ net6.0
false
@@ -32,6 +32,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/Source/QuestPDF.Examples/large-image.jpg b/Source/QuestPDF.Examples/large-image.jpg
new file mode 100644
index 0000000..96b8ace
Binary files /dev/null and b/Source/QuestPDF.Examples/large-image.jpg differ
diff --git a/Source/QuestPDF.UnitTests/ImageTests.cs b/Source/QuestPDF.UnitTests/ImageTests.cs
index b4db73b..9a7dc1b 100644
--- a/Source/QuestPDF.UnitTests/ImageTests.cs
+++ b/Source/QuestPDF.UnitTests/ImageTests.cs
@@ -1,7 +1,12 @@
-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;
@@ -59,5 +64,53 @@ namespace QuestPDF.UnitTests
using var surface = SKSurface.Create(imageInfo);
return surface.Snapshot();
}
+
+ [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);
+ }
+
+ private static int GetDocumentSize(Action container)
+ {
+ return Document
+ .Create(document =>
+ {
+ document.Page(page =>
+ {
+ page.Content().Element(container);
+ });
+ })
+ .GeneratePdf()
+ .Length;
+ }
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj b/Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
index e062de1..7cd338f 100644
--- a/Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
+++ b/Source/QuestPDF.UnitTests/QuestPDF.UnitTests.csproj
@@ -1,7 +1,7 @@
- netcoreapp3.1
+ net6.0
false
diff --git a/Source/QuestPDF/Drawing/DocumentGenerator.cs b/Source/QuestPDF/Drawing/DocumentGenerator.cs
index d982e3b..2727b15 100644
--- a/Source/QuestPDF/Drawing/DocumentGenerator.cs
+++ b/Source/QuestPDF/Drawing/DocumentGenerator.cs
@@ -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,24 +47,26 @@ 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 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 canvas, IDocument document)
+ internal static void RenderDocument(TCanvas canvas, IDocument document, DocumentMetadata metadata)
where TCanvas : ICanvas, IRenderingCanvas
{
var container = new DocumentContainer();
document.Compose(container);
var content = container.Compose();
+ ApplyDefaultImageDpi(content, metadata.RasterDpi);
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
ApplyContentDirection(content, ContentDirection.LeftToRight);
@@ -173,6 +176,15 @@ namespace QuestPDF.Drawing
return debuggingState;
}
+
+ internal static void ApplyDefaultImageDpi(this Element? content, int targetDpi)
+ {
+ content.VisitChildren(x =>
+ {
+ if (x is Image { DocumentImage: { } image })
+ image.TargetDpi ??= targetDpi;
+ });
+ }
internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
{
diff --git a/Source/QuestPDF/Drawing/DocumentMetadata.cs b/Source/QuestPDF/Drawing/DocumentMetadata.cs
index 43420f6..5b77ba6 100644
--- a/Source/QuestPDF/Drawing/DocumentMetadata.cs
+++ b/Source/QuestPDF/Drawing/DocumentMetadata.cs
@@ -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; }
diff --git a/Source/QuestPDF/Elements/Image.cs b/Source/QuestPDF/Elements/Image.cs
index 74ba5a6..d2ea731 100644
--- a/Source/QuestPDF/Elements/Image.cs
+++ b/Source/QuestPDF/Elements/Image.cs
@@ -27,7 +27,7 @@ namespace QuestPDF.Elements
if (DocumentImage == null)
return;
- Canvas.DrawImage(DocumentImage.SkImage, Position.Zero, availableSpace);
+ Canvas.DrawImage(DocumentImage.GetVersionOfSize(availableSpace), Position.Zero, availableSpace);
}
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF/Infrastructure/Image.cs b/Source/QuestPDF/Infrastructure/Image.cs
index 64f5906..61898c8 100644
--- a/Source/QuestPDF/Infrastructure/Image.cs
+++ b/Source/QuestPDF/Infrastructure/Image.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using SkiaSharp;
@@ -7,26 +9,71 @@ namespace QuestPDF.Infrastructure
{
public class Image : IDisposable
{
- internal SKImage SkImage { get; }
+ private SKImage SkImage { get; }
+ internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
+
+ internal int? TargetDpi { get; set; }
+ internal bool PerformScalingToTargetDpi { get; set; } = true;
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;
}
- public Image DisposeAfterDocumentGeneration()
- {
- IsDocumentScoped = true;
- return this;
- }
-
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);
+ ScaledImageCache.Add((targetResolution, scaledImage));
+
+ return scaledImage;
+
+ static SKImage ScaleImage(SKImage originalImage, Size targetSize)
+ {
+ var imageInfo = new SKImageInfo((int)targetSize.Width, (int)targetSize.Height);
+ var target = SKImage.Create(imageInfo);
+ originalImage.ScalePixels(target.PeekPixels(), SKFilterQuality.High);
+
+ return target;
+ }
+
+ 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
@@ -60,5 +107,27 @@ namespace QuestPDF.Infrastructure
}
#endregion
+
+ #region configuration API
+
+ public Image DisposeAfterDocumentGeneration()
+ {
+ IsDocumentScoped = true;
+ return this;
+ }
+
+ public Image WithTargetDpi(int dpi = DocumentMetadata.DefaultPdfDpi)
+ {
+ TargetDpi = dpi;
+ return this;
+ }
+
+ public Image ScaleToTargetDpi(bool value = true)
+ {
+ PerformScalingToTargetDpi = value;
+ return this;
+ }
+
+ #endregion
}
}
\ No newline at end of file