|
|
@@ -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)
|