diff --git a/Source/QuestPDF.Examples/Engine/Helpers.cs b/Source/QuestPDF.Examples/Engine/Helpers.cs deleted file mode 100644 index 1659d08..0000000 --- a/Source/QuestPDF.Examples/Engine/Helpers.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; - -namespace QuestPDF.Examples.Engine -{ - public static class Helpers - { - public static void GeneratePdfAndOpen(this Document document, string? fileName = null) - { - fileName ??= $"{Guid.NewGuid():D}.pdf"; - - var filePath = Path.GetTempPath() + fileName; - var documentData = document.GeneratePdf(); - File.WriteAllBytes(filePath, documentData); - - Process.Start("explorer", filePath); - } - } -} \ No newline at end of file diff --git a/Source/QuestPDF.Examples/ImageExamples.cs b/Source/QuestPDF.Examples/ImageExamples.cs index 5747f26..bf9043e 100644 --- a/Source/QuestPDF.Examples/ImageExamples.cs +++ b/Source/QuestPDF.Examples/ImageExamples.cs @@ -66,29 +66,7 @@ namespace QuestPDF.Examples }); } - [Test] - public void ReusingTheSameImageFileShouldBePossible() - { - var image = Image.FromBinaryData(Placeholders.Image(300, 100)).DisposeAfterDocumentGeneration(); - - RenderingTest - .Create() - .ProducePdf() - .PageSize(PageSizes.A4) - .ShowResults() - .Render(container => - { - container - .Padding(20) - .Column(column => - { - column.Spacing(20); - - foreach (var i in Enumerable.Range(0, 1000)) - column.Item().Image(image); - }); - }); - } + [Test] public void ImageResolutionScaling() @@ -100,18 +78,12 @@ namespace QuestPDF.Examples { document.Page(page => { - page.Size(11000, 11000); + page.Size(210, 210); page.Margin(50); page.Content().Image(image); }); }) - .GeneratePdfAndOpen(); - - //Console.WriteLine(documentData.Length); - - // var filePath = Path.GetTempPath() + $"test.pdf"; - // File.WriteAllBytes(filePath, documentData); - // Console.WriteLine(filePath); + .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 6401446..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 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/Infrastructure/Image.cs b/Source/QuestPDF/Infrastructure/Image.cs index b720b27..61898c8 100644 --- a/Source/QuestPDF/Infrastructure/Image.cs +++ b/Source/QuestPDF/Infrastructure/Image.cs @@ -10,13 +10,17 @@ namespace QuestPDF.Infrastructure public class Image : IDisposable { private SKImage SkImage { get; } - internal List<(Size size, SKImage image)>? ScaledImageCache { get; } + internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; } - public int? TargetDpi { 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) { @@ -31,28 +35,44 @@ namespace QuestPDF.Infrastructure internal SKImage GetVersionOfSize(Size size) { - if (size.Width > Width || size.Height > Height) + 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; - var target = SKImage.Create(new SKImageInfo((int)size.Width, (int)size.Height)); - SkImage.ScalePixels(target.PeekPixels(), SKFilterQuality.High); - - return target; - - // bool ShouldApplyScaling() - // { - // const float tolerance = 1.1f; - // - // if (width > Width / tolerance || height > Height / tolerance) - // return false; - // - // SkImage.sca - // } + ScaledImageCache ??= new List<(Size size, SKImage image)>(); - static bool HasSimilarSize(Size a, Size b, float tolerance) + foreach (var imageCache in ScaledImageCache) { - return (a.Width / b.Width > 1 / tolerance && a.Width / b.Width < tolerance) || - (a.Height / b.Height > 1 / tolerance && a.Height / b.Height < tolerance); + 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; } } @@ -101,7 +121,13 @@ namespace QuestPDF.Infrastructure TargetDpi = dpi; return this; } - + + public Image ScaleToTargetDpi(bool value = true) + { + PerformScalingToTargetDpi = value; + return this; + } + #endregion } } \ No newline at end of file