Browse Source

Implemented automated image scaling

MarcinZiabek 2 năm trước cách đây
mục cha
commit
e53b586943

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

@@ -57,13 +57,6 @@ namespace QuestPDF.UnitTests
                 .MeasureElement(new Size(300, 200))
                 .MeasureElement(new Size(300, 200))
                 .CheckMeasureResult(SpacePlan.FullRender(300, 100));;
                 .CheckMeasureResult(SpacePlan.FullRender(300, 100));;
         }
         }
-        
-        SKImage GenerateImage(int width, int height)
-        {
-            var imageInfo = new SKImageInfo(width, height);
-            using var surface = SKSurface.Create(imageInfo);
-            return surface.Snapshot();
-        }
 
 
         [Test]
         [Test]
         public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize()
         public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize()
@@ -99,6 +92,25 @@ namespace QuestPDF.UnitTests
             (documentWithSingleImageUsedMultipleTimesSize / (float)documentWithSingleImageSize).Should().BeInRange(1f, 1.5f);
             (documentWithSingleImageUsedMultipleTimesSize / (float)documentWithSingleImageSize).Should().BeInRange(1f, 1.5f);
         }
         }
 
 
+        [Test]
+        public void ImageShouldNotBeScaledAboveItsNativeResolution()
+        {
+            var image = Placeholders.Image(200, 200);
+
+            var documentSizeWithScaledDownImage = GetDocumentSize(container => container.Width(100).Height(100).Image(Image.FromBinaryData(image)));
+            //var documentSizeWithNormalImage = GetDocumentSize(container => container.Width(200).Height(200).Image(image));
+            //var documentSizeWithScaledUpImage = GetDocumentSize(container => container.Width(400).Height(400).Image(image));
+        }
+
+        #region helpers
+        
+        SKImage GenerateImage(int width, int height)
+        {
+            var imageInfo = new SKImageInfo(width, height);
+            using var surface = SKSurface.Create(imageInfo);
+            return surface.Snapshot();
+        }
+        
         private static int GetDocumentSize(Action<IContainer> container)
         private static int GetDocumentSize(Action<IContainer> container)
         {
         {
             return Document
             return Document
@@ -112,5 +124,7 @@ namespace QuestPDF.UnitTests
                 .GeneratePdf()
                 .GeneratePdf()
                 .Length;
                 .Length;
         }
         }
+        
+        #endregion
     }
     }
 }
 }

+ 7 - 4
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -66,7 +66,7 @@ namespace QuestPDF.Drawing
             var container = new DocumentContainer();
             var container = new DocumentContainer();
             document.Compose(container);
             document.Compose(container);
             var content = container.Compose();
             var content = container.Compose();
-            ApplyDefaultImageDpi(content, metadata.RasterDpi);
+            ApplyDefaultImageDpi(content, metadata.RasterDpi, metadata.ImageQuality);
             ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
             ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
             ApplyContentDirection(content, ContentDirection.LeftToRight);
             ApplyContentDirection(content, ContentDirection.LeftToRight);
             
             
@@ -177,12 +177,15 @@ namespace QuestPDF.Drawing
             return debuggingState;
             return debuggingState;
         }
         }
 
 
-        internal static void ApplyDefaultImageDpi(this Element? content, int targetDpi)
+        internal static void ApplyDefaultImageDpi(this Element? content, int targetDpi, int targetImageQuality)
         {
         {
             content.VisitChildren(x =>
             content.VisitChildren(x =>
             {
             {
-                if (x is Image { DocumentImage: { } image })
-                    image.TargetDpi ??= targetDpi;
+                if (x is not Image { DocumentImage: { } image })
+                    return;
+                
+                image.TargetDpi ??= targetDpi;
+                image.ImageQuality ??= targetImageQuality;
             });
             });
         }
         }
         
         

+ 68 - 20
Source/QuestPDF/Infrastructure/Image.cs

@@ -11,20 +11,22 @@ namespace QuestPDF.Infrastructure
     {
     {
         private SKImage SkImage { get; }
         private SKImage SkImage { get; }
         internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
         internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
-        
+
         internal int? TargetDpi { get; set; }
         internal int? TargetDpi { get; set; }
-        internal bool PerformScalingToTargetDpi { get; set; } = true;
+        internal int? ImageQuality { get; set; }
+        internal bool PerformScalingToTargetDpi { get; set; }
         internal bool IsDocumentScoped { get; set; }
         internal bool IsDocumentScoped { get; set; }
-        
+
         public int Width => SkImage.Width;
         public int Width => SkImage.Width;
         public int Height => SkImage.Height;
         public int Height => SkImage.Height;
 
 
         private const float ImageSizeSimilarityToleranceMax = 1.1f;
         private const float ImageSizeSimilarityToleranceMax = 1.1f;
         private const float ImageSizeSimilarityToleranceMin = 1 / ImageSizeSimilarityToleranceMax;
         private const float ImageSizeSimilarityToleranceMin = 1 / ImageSizeSimilarityToleranceMax;
-        
+
         private Image(SKImage image)
         private Image(SKImage image)
         {
         {
             SkImage = image;
             SkImage = image;
+            PerformScalingToTargetDpi = image.EncodedData.Size >= Settings.AdjustImageSizeThreshold;
         }
         }
 
 
         public void Dispose()
         public void Dispose()
@@ -40,32 +42,60 @@ namespace QuestPDF.Infrastructure
 
 
             var scalingFactor = TargetDpi.Value / (float)DocumentMetadata.DefaultPdfDpi;
             var scalingFactor = TargetDpi.Value / (float)DocumentMetadata.DefaultPdfDpi;
             var targetResolution = new Size(size.Width * scalingFactor, size.Height * scalingFactor);
             var targetResolution = new Size(size.Width * scalingFactor, size.Height * scalingFactor);
-            
+
             if (targetResolution.Width > Width || targetResolution.Height > Height)
             if (targetResolution.Width > Width || targetResolution.Height > Height)
                 return SkImage;
                 return SkImage;
-            
+
             ScaledImageCache ??= new List<(Size size, SKImage image)>();
             ScaledImageCache ??= new List<(Size size, SKImage image)>();
-            
+
             foreach (var imageCache in ScaledImageCache)
             foreach (var imageCache in ScaledImageCache)
             {
             {
                 if (HasSimilarSize(imageCache.size, targetResolution))
                 if (HasSimilarSize(imageCache.size, targetResolution))
                     return imageCache.image;
                     return imageCache.image;
             }
             }
 
 
-            var scaledImage = ScaleImage(SkImage, targetResolution);
+            var scaledImage = ScaleImage(SkImage, targetResolution, ImageQuality);
             ScaledImageCache.Add((targetResolution, scaledImage));
             ScaledImageCache.Add((targetResolution, scaledImage));
 
 
+            if (SkImage.EncodedData.Size < scaledImage.EncodedData.Size)
+                return SkImage;
+
             return scaledImage;
             return scaledImage;
-     
-            static SKImage ScaleImage(SKImage originalImage, Size targetSize)
+
+            
+            
+            static SKImage ScaleImage(SKImage originalImage, Size targetSize, int? imageQuality)
             {
             {
                 var imageInfo = new SKImageInfo((int)targetSize.Width, (int)targetSize.Height);
                 var imageInfo = new SKImageInfo((int)targetSize.Width, (int)targetSize.Height);
-                var target = SKImage.Create(imageInfo);
+                using var target = SKImage.Create(imageInfo);
                 originalImage.ScalePixels(target.PeekPixels(), SKFilterQuality.High);
                 originalImage.ScalePixels(target.PeekPixels(), SKFilterQuality.High);
 
 
-                return target;
+                var codes = SKCodec.Create(target.EncodedData);
+
+                var targetFormat = imageQuality > 100 ? SKEncodedImageFormat.Png : SKEncodedImageFormat.Jpeg;
+                var targetQuality = Math.Max(imageQuality, 100);
+                var data = target.Encode(targetFormat, targetQuality);
+
+                return SKImage.FromEncodedData(data);
             }
             }
-            
+
+            static (SKEncodedImageFormat format, int quality) GetTargetImageFormat(SKImage originalImage, int? imageQuality) 
+            {
+                if (imageQuality.HasValue)
+                {
+                    var format = imageQuality > 100 
+                        ? SKEncodedImageFormat.Png 
+                        : SKEncodedImageFormat.Jpeg;
+
+                    var quality = Math.Max(imageQuality.Value, 100);
+
+                    return (format, quality);
+                }
+                
+                var codec = SKCodec.Create(originalImage.EncodedData);
+                
+            }
+
             static bool HasSimilarSize(Size a, Size b)
             static bool HasSimilarSize(Size a, Size b)
             {
             {
                 var widthRatio = a.Width / b.Width;
                 var widthRatio = a.Width / b.Width;
@@ -82,40 +112,58 @@ namespace QuestPDF.Infrastructure
         {
         {
             return CreateImage(image);
             return CreateImage(image);
         }
         }
-        
+
         public static Image FromBinaryData(byte[] imageData)
         public static Image FromBinaryData(byte[] imageData)
         {
         {
             return CreateImage(SKImage.FromEncodedData(imageData));
             return CreateImage(SKImage.FromEncodedData(imageData));
         }
         }
-        
+
         public static Image FromFile(string filePath)
         public static Image FromFile(string filePath)
         {
         {
             return CreateImage(SKImage.FromEncodedData(filePath));
             return CreateImage(SKImage.FromEncodedData(filePath));
         }
         }
-        
+
         public static Image FromStream(Stream fileStream)
         public static Image FromStream(Stream fileStream)
         {
         {
             return CreateImage(SKImage.FromEncodedData(fileStream));
             return CreateImage(SKImage.FromEncodedData(fileStream));
         }
         }
-        
+
         private static Image CreateImage(SKImage? image)
         private static Image CreateImage(SKImage? image)
         {
         {
             if (image == null)
             if (image == null)
                 throw new DocumentComposeException("Cannot load or decode provided image.");
                 throw new DocumentComposeException("Cannot load or decode provided image.");
-            
+
             return new Image(image);
             return new Image(image);
         }
         }
 
 
         #endregion
         #endregion
-        
+
         #region configuration API
         #region configuration API
-        
+
         public Image DisposeAfterDocumentGeneration()
         public Image DisposeAfterDocumentGeneration()
         {
         {
             IsDocumentScoped = true;
             IsDocumentScoped = true;
             return this;
             return this;
         }
         }
 
 
+        /// <summary>
+        /// Values from 1 to 100 correspond to the JPEG format, where 1 is lowest and 100 is highest quality.
+        /// Value 101 correspond to the PNG format with a lossless compression and alpha channel support. 
+        /// </summary>
+        /// <param name="quality"></param>
+        /// <returns></returns>
+        public Image WithQuality(int quality)
+        {
+            ImageQuality = quality;
+            return this;
+        }
+        
+        public Image WithQuality(ImageQuality quality)
+        {
+            ImageQuality = (int)quality;
+            return this;
+        }
+
         public Image WithTargetDpi(int dpi = DocumentMetadata.DefaultPdfDpi)
         public Image WithTargetDpi(int dpi = DocumentMetadata.DefaultPdfDpi)
         {
         {
             TargetDpi = dpi;
             TargetDpi = dpi;

+ 40 - 0
Source/QuestPDF/Infrastructure/ImageQuality.cs

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

+ 6 - 0
Source/QuestPDF/Settings.cs

@@ -36,5 +36,11 @@
         /// </summary>
         /// </summary>
         /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
         /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
         public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
         public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
+        
+        
+        /// <summary>
+        /// The file size threshold in bytes that is used to determine if the image should be automatically scaled to physical dimensions.
+        /// </summary>
+        public static int AdjustImageSizeThreshold { get; set; } = 32 * 1024;
     }
     }
 }
 }