From b74e0327fac8a38a879014ead51d3dbfd1f1c7d0 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 20 May 2023 01:16:24 +0200 Subject: [PATCH 1/2] Improved SharedImage API --- Source/QuestPDF.Examples/ImageExamples.cs | 39 ++++++++++-- .../QuestPDF.Examples.csproj | 3 + Source/QuestPDF.Examples/checkbox.png | Bin 0 -> 3525 bytes Source/QuestPDF.UnitTests/ImageTests.cs | 2 +- Source/QuestPDF/Fluent/ImageExtensions.cs | 6 +- Source/QuestPDF/Infrastructure/Image.cs | 57 +++++++++++++++--- 6 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 Source/QuestPDF.Examples/checkbox.png diff --git a/Source/QuestPDF.Examples/ImageExamples.cs b/Source/QuestPDF.Examples/ImageExamples.cs index bd81b65..48d4db1 100644 --- a/Source/QuestPDF.Examples/ImageExamples.cs +++ b/Source/QuestPDF.Examples/ImageExamples.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using NUnit.Framework; using QuestPDF.Drawing.Exceptions; using QuestPDF.Examples.Engine; @@ -79,11 +80,11 @@ namespace QuestPDF.Examples .ShowResults() .Render(page => { - page.Padding(10).Column(column => + page.Padding(15).Column(column => { - column.Spacing(10); + column.Spacing(15); - column.Item().Image("photo.jpg").WithRasterDpi(16); + column.Item().Image("photo.jpg").WithRasterDpi(16).FitUnproportionally(); column.Item().Image("photo.jpg").WithRasterDpi(72); }); }); @@ -99,9 +100,9 @@ namespace QuestPDF.Examples .ShowResults() .Render(page => { - page.Padding(10).Column(column => + page.Padding(15).Column(column => { - column.Spacing(10); + column.Spacing(15); column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72); column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.High).WithRasterDpi(72); @@ -109,6 +110,34 @@ namespace QuestPDF.Examples }); } + [Test] + public void ReusingImage_With() + { + RenderingTest + .Create() + .PageSize(400, 600) + .ProduceImages() + .ShowResults() + .Render(page => + { + page.Padding(15).Column(column => + { + column.Spacing(15); + + var image = Image.FromFile("checkbox.png"); + + foreach (var i in Enumerable.Range(0, 5)) + { + column.Item().Row(row => + { + row.AutoItem().Width(24).Image(image); + row.RelativeItem().PaddingLeft(8).AlignMiddle().Text(Placeholders.Label()).FontSize(16); + }); + } + }); + }); + } + [Test] public void Exception() { diff --git a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj index 5f07fa7..4547520 100644 --- a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj +++ b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj @@ -38,6 +38,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Source/QuestPDF.Examples/checkbox.png b/Source/QuestPDF.Examples/checkbox.png new file mode 100644 index 0000000000000000000000000000000000000000..57422a7a0b52551af918da8724354da15de4c5b6 GIT binary patch literal 3525 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 { - var sharedImage = DocumentImage.FromBinaryData(photo).DisposeAfterDocumentGeneration(); + var sharedImage = DocumentImage.FromBinaryData(photo); foreach (var i in Enumerable.Range(0, 10)) column.Item().Image(sharedImage); diff --git a/Source/QuestPDF/Fluent/ImageExtensions.cs b/Source/QuestPDF/Fluent/ImageExtensions.cs index 2b64836..c8b4fa7 100644 --- a/Source/QuestPDF/Fluent/ImageExtensions.cs +++ b/Source/QuestPDF/Fluent/ImageExtensions.cs @@ -109,19 +109,19 @@ namespace QuestPDF.Fluent { public static ImageDescriptor Image(this IContainer parent, byte[] imageData) { - var image = Infrastructure.Image.FromBinaryData(imageData).DisposeAfterDocumentGeneration(); + var image = Infrastructure.Image.FromBinaryData(imageData); return parent.Image(image); } public static ImageDescriptor Image(this IContainer parent, string filePath) { - var image = Infrastructure.Image.FromFile(filePath).DisposeAfterDocumentGeneration(); + var image = Infrastructure.Image.FromFile(filePath); return parent.Image(image); } public static ImageDescriptor Image(this IContainer parent, Stream fileStream) { - var image = Infrastructure.Image.FromStream(fileStream).DisposeAfterDocumentGeneration(); + var image = Infrastructure.Image.FromStream(fileStream); return parent.Image(image); } diff --git a/Source/QuestPDF/Infrastructure/Image.cs b/Source/QuestPDF/Infrastructure/Image.cs index 5ed6579..eb1bc73 100644 --- a/Source/QuestPDF/Infrastructure/Image.cs +++ b/Source/QuestPDF/Infrastructure/Image.cs @@ -13,28 +13,29 @@ namespace QuestPDF.Infrastructure internal ImageCompressionQuality CompressionQuality { get; set; } } - public class Image : IDisposable + public class Image { internal SKImage SkImage { get; } internal ImageSize Size { get; } - internal bool IsDocumentScoped { get; set; } + internal bool IsDocumentScoped { get; } internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new(); - private Image(SKImage image) + internal Image(SKImage image, bool isDocumentScoped) { SkImage = image; + IsDocumentScoped = isDocumentScoped; Size = new ImageSize(image.Width, image.Height); } - - public void Dispose() + + internal void Dispose() { SkImage.Dispose(); foreach (var cacheKey in ScaledImageCache) cacheKey.image.Dispose(); } - + #region Scaling Image internal SKImage GetVersionOfSize(GetImageVersionRequest request) @@ -79,15 +80,51 @@ namespace QuestPDF.Infrastructure if (image == null) throw new DocumentComposeException("Cannot load or decode provided image."); - return new Image(image); + return new Image(image, true); } #endregion + } - internal Image DisposeAfterDocumentGeneration() + public class SharedImage : Image, IDisposable + { + internal SharedImage(SKImage image) : base(image, false) { - IsDocumentScoped = true; - return this; + } + + public void Dispose() + { + base.Dispose(); + } + + #region public constructors + + public static new SharedImage FromBinaryData(byte[] imageData) + { + return CreateImage(SKImage.FromEncodedData(imageData)); + } + + public static new SharedImage FromFile(string filePath) + { + return CreateImage(SKImage.FromEncodedData(filePath)); + } + + public static new SharedImage FromStream(Stream fileStream) + { + return CreateImage(SKImage.FromEncodedData(fileStream)); + } + + private static SharedImage CreateImage(SKImage? image) + { + if (image == null) + throw new DocumentComposeException("Cannot load or decode provided image."); + + var result = new SharedImage(image); + + return result; + } + + #endregion } } \ No newline at end of file From f60aea0f8393975e7ed375f8579c7b177ca81928 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 20 May 2023 01:52:05 +0200 Subject: [PATCH 2/2] Image generation: improved performance and quality --- Source/QuestPDF/Drawing/DocumentGenerator.cs | 13 ++++++++----- Source/QuestPDF/Elements/Dynamic.cs | 5 ++++- Source/QuestPDF/Elements/DynamicImage.cs | 8 ++++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Source/QuestPDF/Drawing/DocumentGenerator.cs b/Source/QuestPDF/Drawing/DocumentGenerator.cs index 05a458e..86e9062 100644 --- a/Source/QuestPDF/Drawing/DocumentGenerator.cs +++ b/Source/QuestPDF/Drawing/DocumentGenerator.cs @@ -51,7 +51,7 @@ namespace QuestPDF.Drawing var settings = document.GetSettings(); var canvas = new ImageCanvas(settings); - RenderDocument(canvas, document, settings); + RenderDocument(canvas, document, settings, useOriginalImages: true); return canvas.Images; } @@ -92,7 +92,7 @@ namespace QuestPDF.Drawing return canvas.Pictures; } - internal static void RenderDocument(TCanvas canvas, IDocument document, DocumentSettings settings) + internal static void RenderDocument(TCanvas canvas, IDocument document, DocumentSettings settings, bool useOriginalImages = false) where TCanvas : ICanvas, IRenderingCanvas { var container = new DocumentContainer(); @@ -101,7 +101,7 @@ namespace QuestPDF.Drawing ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default); ApplyContentDirection(content, settings.ContentDirection); - ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality); + ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages); var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null; @@ -228,7 +228,7 @@ namespace QuestPDF.Drawing ApplyContentDirection(child, direction); } - internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality) + internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality, bool useOriginalImages) { content.VisitChildren(x => { @@ -236,25 +236,28 @@ namespace QuestPDF.Drawing { image.TargetDpi ??= imageRasterDpi; image.CompressionQuality ??= imageCompressionQuality; + image.UseOriginalImage |= useOriginalImages; } if (x is QuestPDF.Elements.DynamicImage dynamicImage) { dynamicImage.TargetDpi ??= imageRasterDpi; dynamicImage.CompressionQuality ??= imageCompressionQuality; + dynamicImage.UseOriginalImage |= useOriginalImages; } if (x is DynamicHost dynamicHost) { dynamicHost.ImageTargetDpi ??= imageRasterDpi; dynamicHost.ImageCompressionQuality ??= imageCompressionQuality; + dynamicHost.UseOriginalImage |= useOriginalImages; } if (x is TextBlock textBlock) { foreach (var textBlockElement in textBlock.Items.OfType()) { - textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality); + textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality, useOriginalImages); } } }); diff --git a/Source/QuestPDF/Elements/Dynamic.cs b/Source/QuestPDF/Elements/Dynamic.cs index 3d1dbcc..f15a404 100644 --- a/Source/QuestPDF/Elements/Dynamic.cs +++ b/Source/QuestPDF/Elements/Dynamic.cs @@ -16,6 +16,7 @@ namespace QuestPDF.Elements internal int? ImageTargetDpi { get; set; } internal ImageCompressionQuality? ImageCompressionQuality { get; set; } + internal bool UseOriginalImage { get; set; } public DynamicHost(DynamicComponentProxy child) { @@ -63,6 +64,7 @@ namespace QuestPDF.Elements ImageTargetDpi = ImageTargetDpi.Value, ImageCompressionQuality = ImageCompressionQuality.Value, + UseOriginalImage = UseOriginalImage, PageNumber = PageContext.CurrentPage, TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd, @@ -88,6 +90,7 @@ namespace QuestPDF.Elements internal int ImageTargetDpi { get; set; } internal ImageCompressionQuality ImageCompressionQuality { get; set; } + internal bool UseOriginalImage { get; set; } public int PageNumber { get; internal set; } public int TotalPages { get; internal set; } @@ -100,7 +103,7 @@ namespace QuestPDF.Elements container.ApplyInheritedAndGlobalTexStyle(TextStyle); container.ApplyContentDirection(ContentDirection); - container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality); + container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage); container.InjectDependencies(PageContext, Canvas); container.VisitChildren(x => (x as IStateResettable)?.ResetState()); diff --git a/Source/QuestPDF/Elements/DynamicImage.cs b/Source/QuestPDF/Elements/DynamicImage.cs index 4441c63..d5d7028 100644 --- a/Source/QuestPDF/Elements/DynamicImage.cs +++ b/Source/QuestPDF/Elements/DynamicImage.cs @@ -13,6 +13,7 @@ namespace QuestPDF.Elements { internal int? TargetDpi { get; set; } internal ImageCompressionQuality? CompressionQuality { get; set; } + internal bool UseOriginalImage { get; set; } public GenerateDynamicImageDelegate? Source { get; set; } internal override SpacePlan Measure(Size availableSpace) @@ -31,6 +32,13 @@ namespace QuestPDF.Elements return; using var originalImage = SKImage.FromEncodedData(imageData); + + if (UseOriginalImage) + { + Canvas.DrawImage(originalImage, Position.Zero, availableSpace); + return; + } + using var compressedImage = originalImage.CompressImage(CompressionQuality.Value); var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);