Browse Source

Integrated new Image API

MarcinZiabek 2 years ago
parent
commit
c21a335342

+ 18 - 0
Source/QuestPDF.Examples/ImageExamples.cs

@@ -50,6 +50,24 @@ namespace QuestPDF.Examples
                 });
         }
         
+        [Test]
+        public void ScalingImageWithAlpha()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(PageSizes.A4)
+                .ProducePdf()
+                .ShowResults()
+                .Render(page =>
+                {
+                    page.Padding(25).Layers(layers =>
+                    {
+                        layers.Layer().Image(Placeholders.Image);
+                        layers.PrimaryLayer().Padding(25).Image("multilingual.png");
+                    });
+                });
+        }
+        
         [Test]
         public void Exception()
         {

+ 3 - 0
Source/QuestPDF.Examples/QuestPDF.Examples.csproj

@@ -32,6 +32,9 @@
       <None Update="pdf-icon.svg">
         <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
       </None>
+      <None Update="multilingual.png">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </None>
     </ItemGroup>
 
 </Project>

BIN
Source/QuestPDF.Examples/multilingual.png


+ 3 - 1
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -236,7 +236,9 @@ namespace QuestPDF.Drawing
                     return;
                 
                 image.TargetDpi ??= settings.RasterDpi;
-                image.TargetQuality ??= settings.ImageQuality;
+                image.ScalingQuality ??= settings.ImageScalingQuality;
+                image.ScalingStrategy ??= settings.ImageScalingStrategy;
+                image.CompressionQuality ??= settings.ImageCompressionQuality;
             });
         }
 

+ 1 - 1
Source/QuestPDF/Drawing/PdfCanvas.cs

@@ -42,7 +42,7 @@ namespace QuestPDF.Drawing
                 Modified = metadata.ModifiedDate,
                 
                 RasterDpi = documentSettings.RasterDpi,
-                EncodingQuality = documentSettings.ImageQuality,
+                EncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
                 PdfA = documentSettings.PdfA
             };
         }

+ 2 - 1
Source/QuestPDF/Elements/Image.cs

@@ -27,7 +27,8 @@ namespace QuestPDF.Elements
             if (DocumentImage == null)
                 return;
 
-            Canvas.DrawImage(DocumentImage.SkImage, Position.Zero, availableSpace);
+            var image = DocumentImage.GetVersionOfSize(availableSpace);
+            Canvas.DrawImage(image, Position.Zero, availableSpace);
         }
     }
 }

+ 0 - 0
Source/QuestPDF/Fluent/ImageExtensions.cs → Source/QuestPDF/Fluent/ImageElementExtensions.cs


+ 62 - 0
Source/QuestPDF/Fluent/ImageModelExtensions.cs

@@ -0,0 +1,62 @@
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public static class ImageModelExtensions
+    {
+        /// <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 static Image DisposeAfterDocumentGeneration(this Image image)
+        {
+            image.IsDocumentScoped = true;
+            return image;
+        }
+
+        /// <summary>
+        /// When enabled, the library will not attempt to resize the image to fit the target DPI, nor save it with target image quality.
+        /// </summary>
+        public static Image UseOriginalImage(this Image image)
+        {
+            image.UseOriginalImage = true;
+            return image;
+        }
+        
+        /// <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 static Image WithRasterDpi(this Image image, int dpi)
+        {
+            image.TargetDpi = dpi;
+            return image;
+        }
+
+        /// <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 static Image WithCompressionQuality(this Image image, ImageCompressionQuality quality)
+        {
+            image.CompressionQuality = quality;
+            return image;
+        }
+        
+        public static Image WithScalingQuality(this Image image, ImageScalingQuality strategy)
+        {
+            image.ScalingQuality = strategy;
+            return image;
+        }
+        
+        public static Image WithScalingStrategy(this Image image, ImageScalingStrategy strategy)
+        {
+            image.ScalingStrategy = strategy;
+            return image;
+        }
+    }
+}

+ 27 - 0
Source/QuestPDF/Helpers/Helpers.cs

@@ -6,6 +6,7 @@ using System.Reflection;
 using System.Text.RegularExpressions;
 using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
+using SkiaSharp;
 
 namespace QuestPDF.Helpers
 {
@@ -60,5 +61,31 @@ namespace QuestPDF.Helpers
         {
             return size.Width < 0f || size.Height < 0f;
         }
+        
+        internal static int ToQualityValue(this ImageCompressionQuality quality)
+        {
+            return quality switch
+            {
+                ImageCompressionQuality.Best => 100,
+                ImageCompressionQuality.VeryHigh => 90,
+                ImageCompressionQuality.High => 80,
+                ImageCompressionQuality.Medium => 60,
+                ImageCompressionQuality.Low => 40,
+                ImageCompressionQuality.VeryLow => 20,
+                _ => throw new ArgumentOutOfRangeException(nameof(quality), quality, null)
+            };
+        }
+
+        internal static SKFilterQuality ToFilterQuality(this ImageScalingQuality quality)
+        {
+            return quality switch
+            {
+                ImageScalingQuality.Low => SKFilterQuality.None,
+                ImageScalingQuality.Medium => SKFilterQuality.Low,
+                ImageScalingQuality.High => SKFilterQuality.Medium,
+                ImageScalingQuality.Best => SKFilterQuality.High,
+                _ => throw new ArgumentOutOfRangeException(nameof(quality), quality, null)
+            };
+        }
     }
 }

+ 8 - 5
Source/QuestPDF/Infrastructure/DocumentSettings.cs

@@ -1,6 +1,4 @@
-using ImageQualityEnum = QuestPDF.Infrastructure.ImageQuality;
-
-namespace QuestPDF.Infrastructure
+namespace QuestPDF.Infrastructure
 {
     public class DocumentSettings
     {
@@ -19,7 +17,12 @@ namespace QuestPDF.Infrastructure
         /// 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 int ImageQuality { get; set; } = (int)ImageQualityEnum.VeryHigh;
+        public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
+        
+        // TODO: add comments
+        public ImageScalingStrategy ImageScalingStrategy { get; set; } = ImageScalingStrategy.ScaleOnlyToSignificantlySmallerResolution;
+        
+        public ImageScalingQuality ImageScalingQuality { get; set; } = ImageScalingQuality.High;
         
         /// <summary>
         /// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
@@ -28,7 +31,7 @@ namespace QuestPDF.Infrastructure
         /// Default value is 72.
         /// </summary>
         public int RasterDpi { get; set; } = DefaultRasterDpi;
-
+ 
         public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
         
         public static DocumentSettings Default => new DocumentSettings();

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

@@ -4,7 +4,7 @@ namespace QuestPDF.Infrastructure
 {
     public interface IDocument
     {
-#if NETCOREAPP2_1_OR_GREATER
+#if NETCOREAPP3_0_OR_GREATER
         public DocumentMetadata GetMetadata() => DocumentMetadata.Default; 
         public DocumentSettings GetSettings() => DocumentSettings.Default;
 #else

+ 81 - 37
Source/QuestPDF/Infrastructure/Image.cs

@@ -1,21 +1,30 @@
 using System;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.IO;
 using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Helpers;
 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; }
-
+        private SKImage SkImage { get; }
         public int Width => SkImage.Width;
         public int Height => SkImage.Height;
 
+        internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
+        
+        internal bool IsDocumentScoped { get; set; }
+        internal bool UseOriginalImage { get; set; }
+        internal int? TargetDpi { get; set; }
+        internal ImageCompressionQuality? CompressionQuality { get; set; }
+        internal ImageScalingQuality? ScalingQuality { get; set; }
+        internal ImageScalingStrategy? ScalingStrategy { get; set; }
+
+        private const float ImageSizeSimilarityToleranceMax = 0.75f;
+        
         private Image(SKImage image)
         {
             SkImage = image;
@@ -24,48 +33,83 @@ namespace QuestPDF.Infrastructure
         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()
+        #region Scaling Image
+
+        internal SKImage GetVersionOfSize(Size availableAreaSize)
         {
-            IsDocumentScoped = true;
-            return this;
+            if (UseOriginalImage)
+                return SkImage;
+
+            var imageResolution = new Size(SkImage.Width, SkImage.Height);
+            var targetResolution = GetTargetResolution(ScalingStrategy.Value, imageResolution, availableAreaSize, TargetDpi ?? DocumentSettings.DefaultRasterDpi);
+            
+            return ScaleAndCompressImage(SkImage, targetResolution, ScalingQuality.Value, CompressionQuality.Value);
         }
+        
+        private static Size GetTargetResolution(ImageScalingStrategy scalingStrategy, Size imageResolution, Size availableAreaSize, int targetDpi)
+        {
+            if (scalingStrategy == ImageScalingStrategy.Never)
+                return imageResolution;
+            
+            if (scalingStrategy == ImageScalingStrategy.Always)
+                return availableAreaSize;
+            
+            var scalingFactor = targetDpi / DocumentSettings.DefaultRasterDpi;
+            var targetResolution = new Size(availableAreaSize.Width * scalingFactor, availableAreaSize.Height * scalingFactor);
+
+            var isSignificantlySmaller = IsImageSignificantlySmallerThanDrawingArea(imageResolution, targetResolution);
+            
+            if (scalingStrategy == ImageScalingStrategy.ScaleOnlyToSignificantlySmallerResolution && isSignificantlySmaller)
+                return targetResolution;
+
+            var isSmaller = IsImageSmallerThanDrawingArea(imageResolution, targetResolution);
+
+            if (scalingStrategy == ImageScalingStrategy.ScaleOnlyToSmallerResolution && isSmaller)
+                return targetResolution;
 
-        /// <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)
+            return imageResolution;
+        }
+        
+        private static SKImage CompressImage(SKImage image, ImageCompressionQuality compressionQuality)
         {
-            TargetDpi = dpi;
-            return this;
+            var targetFormat = image.Info.IsOpaque 
+                ? SKEncodedImageFormat.Png 
+                : SKEncodedImageFormat.Jpeg;
+
+            if (targetFormat == SKEncodedImageFormat.Png)
+                compressionQuality = ImageCompressionQuality.Best;
+            
+            var data = image.Encode(targetFormat, compressionQuality.ToQualityValue());
+            return SKImage.FromEncodedData(data);
         }
 
-        /// <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)
+        private static SKImage ScaleAndCompressImage(SKImage image, Size targetResolution, ImageScalingQuality scalingQuality, ImageCompressionQuality compressionQuality)
         {
-            if (value is not (>= 1 and <= 101))
-                throw new DocumentComposeException("Image quality must be between 1 and 101.");
+            var imageInfo = new SKImageInfo((int)targetResolution.Width, (int)targetResolution.Height);
+            
+            using var resultImage = SKImage.Create(imageInfo);
+            image.ScalePixels(resultImage.PeekPixels(), scalingQuality.ToFilterQuality());
             
-            TargetQuality = value;
-            return this;
+            return CompressImage(resultImage, compressionQuality);
         }
-
-        #endregion
         
+        private static bool IsImageSmallerThanDrawingArea(Size imageResolution, Size targetResolution)
+        {
+            return imageResolution.Width < targetResolution.Width || imageResolution.Height < targetResolution.Height;
+        }
+        
+        private static bool IsImageSignificantlySmallerThanDrawingArea(Size imageResolution, Size targetResolution, float sizeSimilarityThreshold = ImageSizeSimilarityToleranceMax)
+        {
+            var widthRatio = imageResolution.Width / targetResolution.Width;
+            var heightRatio = imageResolution.Height / targetResolution.Height;
+        
+            return widthRatio < sizeSimilarityThreshold && heightRatio < sizeSimilarityThreshold;
+        }
+        
+        #endregion
+
         #region public constructors
 
         internal static Image FromSkImage(SKImage image)

+ 7 - 12
Source/QuestPDF/Infrastructure/ImageQuality.cs → Source/QuestPDF/Infrastructure/ImageCompressionQuality.cs

@@ -1,40 +1,35 @@
 namespace QuestPDF.Infrastructure
 {
-    public enum ImageQuality
+    public enum ImageCompressionQuality
     {
-        /// <summary>
-        /// PNG format with alpha support
-        /// </summary>
-        Lossless = 101,
-
         /// <summary>
         /// JPEG format with compression set to 100 out of 100
         /// </summary>
-        Max = 100,
+        Best,
 
         /// <summary>
         /// JPEG format with compression set to 90 out of 100
         /// </summary>
-        VeryHigh = 90,
+        VeryHigh,
 
         /// <summary>
         /// JPEG format with compression set to 80 out of 100
         /// </summary>
-        High = 80,
+        High,
 
         /// <summary>
         /// JPEG format with compression set to 60 out of 100
         /// </summary>
-        Medium = 60,
+        Medium,
 
         /// <summary>
         /// JPEG format with compression set to 40 out of 100
         /// </summary>
-        Low = 40,
+        Low,
 
         /// <summary>
         /// JPEG format with compression set to 20 out of 100
         /// </summary>
-        VeryLow = 20
+        VeryLow
     }
 }

+ 15 - 0
Source/QuestPDF/Infrastructure/ImageScalingQuality.cs

@@ -0,0 +1,15 @@
+namespace QuestPDF.Infrastructure
+{
+    // https://chromium.googlesource.com/skia/+/master/include/core/SkFilterQuality.h
+    public enum ImageScalingQuality
+    {
+        // TODO: add comments
+        Low,
+        
+        Medium,
+        
+        High,
+        
+        Best
+    }
+}

+ 11 - 0
Source/QuestPDF/Infrastructure/ImageScalingStrategy.cs

@@ -0,0 +1,11 @@
+namespace QuestPDF.Infrastructure
+{
+    public enum ImageScalingStrategy
+    {
+        // TODO: add comments
+        Always,
+        ScaleOnlyToSmallerResolution,
+        ScaleOnlyToSignificantlySmallerResolution,
+        Never
+    }
+}