Browse Source

Introduced global default image settings

MarcinZiabek 2 years ago
parent
commit
2040822a37

+ 76 - 7
Source/QuestPDF.UnitTests/ImageTests.cs

@@ -1,10 +1,17 @@
-using NUnit.Framework;
+using System;
+using System.Linq;
+using System.Net.Mime;
+using FluentAssertions;
+using NUnit.Framework;
 using QuestPDF.Drawing;
 using QuestPDF.Drawing;
 using QuestPDF.Elements;
 using QuestPDF.Elements;
 using QuestPDF.Fluent;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
 using QuestPDF.UnitTests.TestEngine;
 using SkiaSharp;
 using SkiaSharp;
+using ImageElement = QuestPDF.Elements.Image;
+using DocumentImage = QuestPDF.Infrastructure.Image;
 
 
 namespace QuestPDF.UnitTests
 namespace QuestPDF.UnitTests
 {
 {
@@ -15,9 +22,9 @@ namespace QuestPDF.UnitTests
         public void Measure_TakesAvailableSpaceRegardlessOfSize()
         public void Measure_TakesAvailableSpaceRegardlessOfSize()
         {
         {
             TestPlan
             TestPlan
-                .For(x => new Image
+                .For(x => new ImageElement
                 {
                 {
-                    InternalImage = GenerateImage(400, 300)
+                    DocumentImage = GenerateDocumentImage(400, 300)
                 })
                 })
                 .MeasureElement(new Size(300, 200))
                 .MeasureElement(new Size(300, 200))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
@@ -27,9 +34,9 @@ namespace QuestPDF.UnitTests
         public void Draw_TakesAvailableSpaceRegardlessOfSize()
         public void Draw_TakesAvailableSpaceRegardlessOfSize()
         {
         {
             TestPlan
             TestPlan
-                .For(x => new Image
+                .For(x => new ImageElement
                 {
                 {
-                    InternalImage = GenerateImage(400, 300)
+                    DocumentImage = GenerateDocumentImage(400, 300)
                 })
                 })
                 .DrawElement(new Size(300, 200))
                 .DrawElement(new Size(300, 200))
                 .ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
                 .ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
@@ -39,7 +46,7 @@ namespace QuestPDF.UnitTests
         [Test]
         [Test]
         public void Fluent_RecognizesImageProportions()
         public void Fluent_RecognizesImageProportions()
         {
         {
-            var image = GenerateImage(600, 200).Encode(SKEncodedImageFormat.Png, 100).ToArray();
+            var image = GenerateSkiaImage(600, 200).Encode(SKEncodedImageFormat.Png, 100).ToArray();
             
             
             TestPlan
             TestPlan
                 .For(x =>
                 .For(x =>
@@ -52,11 +59,73 @@ namespace QuestPDF.UnitTests
                 .CheckMeasureResult(SpacePlan.FullRender(300, 100));;
                 .CheckMeasureResult(SpacePlan.FullRender(300, 100));;
         }
         }
         
         
-        SKImage GenerateImage(int width, int height)
+        [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 = DocumentImage.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);
+
+            // TODO
+        }
+        
+        private static int GetDocumentSize(Action<IContainer> container)
+        {
+            return Document
+                .Create(document =>
+                {
+                    document.Page(page =>
+                    {
+                        page.Content().Element(container);
+                    });
+                })
+                .GeneratePdf()
+                .Length;
+        }
+        
+        SKImage GenerateSkiaImage(int width, int height)
         {
         {
             var imageInfo = new SKImageInfo(width, height);
             var imageInfo = new SKImageInfo(width, height);
             using var surface = SKSurface.Create(imageInfo);
             using var surface = SKSurface.Create(imageInfo);
             return surface.Snapshot();
             return surface.Snapshot();
         }
         }
+        
+        DocumentImage GenerateDocumentImage(int width, int height)
+        {
+            var skiaImage = GenerateSkiaImage(width, height);
+            return DocumentImage.FromSkImage(skiaImage);
+        }
     }
     }
 }
 }

+ 15 - 0
Source/QuestPDF.UnitTests/LicenseSetup.cs

@@ -0,0 +1,15 @@
+using NUnit.Framework;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.UnitTests
+{
+    [SetUpFixture]
+    public class LicenseSetup
+    {
+        [OneTimeSetUp]
+        public static void Setup()
+        {
+            QuestPDF.Settings.License = LicenseType.Community;
+        }
+    }
+}

+ 20 - 6
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -23,7 +23,7 @@ namespace QuestPDF.Drawing
             var metadata = document.GetMetadata();
             var metadata = document.GetMetadata();
             var settings = document.GetSettings();
             var settings = document.GetSettings();
             var canvas = new PdfCanvas(stream, metadata, settings);
             var canvas = new PdfCanvas(stream, metadata, settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
         }
         }
         
         
         internal static void GenerateXps(Stream stream, IDocument document)
         internal static void GenerateXps(Stream stream, IDocument document)
@@ -33,7 +33,7 @@ namespace QuestPDF.Drawing
             
             
             var settings = document.GetSettings();
             var settings = document.GetSettings();
             var canvas = new XpsCanvas(stream, settings);
             var canvas = new XpsCanvas(stream, settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
         }
         }
 
 
         private static void CheckIfStreamIsCompatible(Stream stream)
         private static void CheckIfStreamIsCompatible(Stream stream)
@@ -51,7 +51,7 @@ namespace QuestPDF.Drawing
             
             
             var settings = document.GetSettings();
             var settings = document.GetSettings();
             var canvas = new ImageCanvas(settings);
             var canvas = new ImageCanvas(settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
 
 
             return canvas.Images;
             return canvas.Images;
         }
         }
@@ -88,18 +88,20 @@ namespace QuestPDF.Drawing
         internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
         internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
         {
         {
             var canvas = new SkiaPictureCanvas();
             var canvas = new SkiaPictureCanvas();
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, DocumentSettings.Default);
             return canvas.Pictures;
             return canvas.Pictures;
         }
         }
         
         
-        internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
+        internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings)
             where TCanvas : ICanvas, IRenderingCanvas
             where TCanvas : ICanvas, IRenderingCanvas
         {
         {
             var container = new DocumentContainer();
             var container = new DocumentContainer();
             document.Compose(container);
             document.Compose(container);
             var content = container.Compose();
             var content = container.Compose();
+            
             ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
             ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
-            ApplyContentDirection(content, ContentDirection.LeftToRight);
+            ApplyContentDirection(content, settings.ContentDirection);
+            ApplyDefaultImageConfiguration(content, settings);
             
             
             var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
             var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
             
             
@@ -225,6 +227,18 @@ namespace QuestPDF.Drawing
             foreach (var child in content.GetChildren())
             foreach (var child in content.GetChildren())
                 ApplyContentDirection(child, direction);
                 ApplyContentDirection(child, direction);
         }
         }
+        
+        internal static void ApplyDefaultImageConfiguration(this Element? content, DocumentSettings settings)
+        {
+            content.VisitChildren(x =>
+            {
+                if (x is not QuestPDF.Elements.Image { DocumentImage: { } image })
+                    return;
+                
+                image.TargetDpi ??= settings.RasterDpi;
+                image.TargetQuality ??= settings.ImageQuality;
+            });
+        }
 
 
         internal static void ApplyInheritedAndGlobalTexStyle(this Element? content, TextStyle documentDefaultTextStyle)
         internal static void ApplyInheritedAndGlobalTexStyle(this Element? content, TextStyle documentDefaultTextStyle)
         {
         {

+ 5 - 4
Source/QuestPDF/Elements/Image.cs

@@ -7,11 +7,12 @@ namespace QuestPDF.Elements
 {
 {
     internal class Image : Element, ICacheable
     internal class Image : Element, ICacheable
     {
     {
-        public SKImage? InternalImage { get; set; }
+        public Infrastructure.Image? DocumentImage { get; set; }
 
 
         ~Image()
         ~Image()
         {
         {
-            InternalImage?.Dispose();
+            if (DocumentImage is { IsDocumentScoped: true })
+                DocumentImage?.Dispose();
         }
         }
         
         
         internal override SpacePlan Measure(Size availableSpace)
         internal override SpacePlan Measure(Size availableSpace)
@@ -23,10 +24,10 @@ namespace QuestPDF.Elements
 
 
         internal override void Draw(Size availableSpace)
         internal override void Draw(Size availableSpace)
         {
         {
-            if (InternalImage == null)
+            if (DocumentImage == null)
                 return;
                 return;
 
 
-            Canvas.DrawImage(InternalImage, Position.Zero, availableSpace);
+            Canvas.DrawImage(DocumentImage.SkImage, Position.Zero, availableSpace);
         }
         }
     }
     }
 }
 }

+ 6 - 6
Source/QuestPDF/Fluent/ImageExtensions.cs

@@ -11,30 +11,30 @@ namespace QuestPDF.Fluent
     {
     {
         public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
         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);
             parent.Image(image, scaling);
         }
         }
         
         
         public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
         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);
             parent.Image(image, scaling);
         }
         }
         
         
         public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
         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);
             parent.Image(image, scaling);
         }
         }
         
         
-        private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth)
+        internal static void Image(this IContainer parent, Infrastructure.Image image, ImageScaling scaling = ImageScaling.FitWidth)
         {
         {
             if (image == null)
             if (image == null)
                 throw new DocumentComposeException("Cannot load or decode provided image.");
                 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)
             if (scaling != ImageScaling.Resize)

+ 1 - 1
Source/QuestPDF/Infrastructure/ContentDirection.cs

@@ -1,6 +1,6 @@
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
-    internal enum ContentDirection
+    public enum ContentDirection
     {
     {
         LeftToRight,
         LeftToRight,
         RightToLeft
         RightToLeft

+ 6 - 1
Source/QuestPDF/Infrastructure/DocumentSettings.cs

@@ -4,6 +4,8 @@ namespace QuestPDF.Infrastructure
 {
 {
     public class DocumentSettings
     public class DocumentSettings
     {
     {
+        public const int DefaultRasterDpi = 72;
+        
         /// <summary>
         /// <summary>
         /// Gets or sets a value indicating whether or not make the document PDF/A-2b conformant.
         /// Gets or sets a value indicating whether or not make the document PDF/A-2b conformant.
         /// If true, include XMP metadata, a document UUID, and sRGB output intent information.
         /// If true, include XMP metadata, a document UUID, and sRGB output intent information.
@@ -23,8 +25,11 @@ namespace QuestPDF.Infrastructure
         /// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
         /// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
         /// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
         /// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
         /// When generating images, this parameter also controls the resolution of the generated content.
         /// When generating images, this parameter also controls the resolution of the generated content.
+        /// Default value is 72.
         /// </summary>
         /// </summary>
-        public int RasterDpi { get; set; } = 72;
+        public int RasterDpi { get; set; } = DefaultRasterDpi;
+
+        public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
         
         
         public static DocumentSettings Default => new DocumentSettings();
         public static DocumentSettings Default => new DocumentSettings();
     }
     }

+ 101 - 0
Source/QuestPDF/Infrastructure/Image.cs

@@ -0,0 +1,101 @@
+using System;
+using System.IO;
+using QuestPDF.Drawing.Exceptions;
+using SkiaSharp;
+
+namespace QuestPDF.Infrastructure
+{
+    public class Image : IDisposable
+    {
+        internal SKImage SkImage { get; }
+        
+        internal int? TargetDpi { get; set; }
+        internal int? TargetQuality { get; set; }
+        internal bool IsDocumentScoped { get; set; }
+
+        public int Width => SkImage.Width;
+        public int Height => SkImage.Height;
+
+        private Image(SKImage image)
+        {
+            SkImage = image;
+        }
+
+        public void Dispose()
+        {
+            SkImage.Dispose();
+        }
+
+        #region Fluent API
+        
+        /// <summary>
+        /// When enabled, the image object is disposed automatically after document generation, and you don't need to call the Dispose method yourself.
+        /// </summary>
+        public Image DisposeAfterDocumentGeneration()
+        {
+            IsDocumentScoped = true;
+            return this;
+        }
+
+        /// <summary>
+        /// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
+        /// A larger DPI would create a PDF that reflects the original intent with better fidelity, but it can make for larger PDF files too, which would use more memory while rendering, and it would be slower to be processed or sent online or to printer.
+        /// When generating images, this parameter also controls the resolution of the generated content.
+        /// Default value is 72.
+        /// </summary>
+        public Image WithRasterDpi(int dpi)
+        {
+            TargetDpi = dpi;
+            return this;
+        }
+
+        /// <summary>
+        /// Encoding quality controls the trade-off between size and quality.
+        /// The value 101 corresponds to lossless encoding.
+        /// If this value is set to a value between 1 and 100, and the image is opaque, it will be encoded using the JPEG format with that quality setting.
+        /// The default value is 90 (very high quality).
+        /// </summary>
+        public Image WithQuality(int value)
+        {
+            if (value is not (>= 1 and <= 101))
+                throw new DocumentComposeException("Image quality must be between 1 and 101.");
+            
+            TargetQuality = value;
+            return this;
+        }
+
+        #endregion
+        
+        #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
+    }
+}