瀏覽代碼

Introduced global default image settings

MarcinZiabek 2 年之前
父節點
當前提交
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.Elements;
 using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
 using SkiaSharp;
+using ImageElement = QuestPDF.Elements.Image;
+using DocumentImage = QuestPDF.Infrastructure.Image;
 
 namespace QuestPDF.UnitTests
 {
@@ -15,9 +22,9 @@ namespace QuestPDF.UnitTests
         public void Measure_TakesAvailableSpaceRegardlessOfSize()
         {
             TestPlan
-                .For(x => new Image
+                .For(x => new ImageElement
                 {
-                    InternalImage = GenerateImage(400, 300)
+                    DocumentImage = GenerateDocumentImage(400, 300)
                 })
                 .MeasureElement(new Size(300, 200))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 200));
@@ -27,9 +34,9 @@ namespace QuestPDF.UnitTests
         public void Draw_TakesAvailableSpaceRegardlessOfSize()
         {
             TestPlan
-                .For(x => new Image
+                .For(x => new ImageElement
                 {
-                    InternalImage = GenerateImage(400, 300)
+                    DocumentImage = GenerateDocumentImage(400, 300)
                 })
                 .DrawElement(new Size(300, 200))
                 .ExpectCanvasDrawImage(new Position(0, 0), new Size(300, 200))
@@ -39,7 +46,7 @@ namespace QuestPDF.UnitTests
         [Test]
         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
                 .For(x =>
@@ -52,11 +59,73 @@ namespace QuestPDF.UnitTests
                 .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);
             using var surface = SKSurface.Create(imageInfo);
             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 settings = document.GetSettings();
             var canvas = new PdfCanvas(stream, metadata, settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
         }
         
         internal static void GenerateXps(Stream stream, IDocument document)
@@ -33,7 +33,7 @@ namespace QuestPDF.Drawing
             
             var settings = document.GetSettings();
             var canvas = new XpsCanvas(stream, settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
         }
 
         private static void CheckIfStreamIsCompatible(Stream stream)
@@ -51,7 +51,7 @@ namespace QuestPDF.Drawing
             
             var settings = document.GetSettings();
             var canvas = new ImageCanvas(settings);
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, settings);
 
             return canvas.Images;
         }
@@ -88,18 +88,20 @@ namespace QuestPDF.Drawing
         internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
         {
             var canvas = new SkiaPictureCanvas();
-            RenderDocument(canvas, document);
+            RenderDocument(canvas, document, DocumentSettings.Default);
             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
         {
             var container = new DocumentContainer();
             document.Compose(container);
             var content = container.Compose();
+            
             ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
-            ApplyContentDirection(content, ContentDirection.LeftToRight);
+            ApplyContentDirection(content, settings.ContentDirection);
+            ApplyDefaultImageConfiguration(content, settings);
             
             var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
             
@@ -225,6 +227,18 @@ namespace QuestPDF.Drawing
             foreach (var child in content.GetChildren())
                 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)
         {

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

@@ -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.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)
         {
-            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)
+        internal 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)

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

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

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

@@ -4,6 +4,8 @@ namespace QuestPDF.Infrastructure
 {
     public class DocumentSettings
     {
+        public const int DefaultRasterDpi = 72;
+        
         /// <summary>
         /// 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.
@@ -23,8 +25,11 @@ namespace QuestPDF.Infrastructure
         /// 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 int RasterDpi { get; set; } = 72;
+        public int RasterDpi { get; set; } = DefaultRasterDpi;
+
+        public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
         
         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
+    }
+}