diff --git a/Source/QuestPDF.Examples/ImageExamples.cs b/Source/QuestPDF.Examples/ImageExamples.cs
index 91bf3af..a44791a 100644
--- a/Source/QuestPDF.Examples/ImageExamples.cs
+++ b/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()
{
diff --git a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
index a053de7..56e2ce5 100644
--- a/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
+++ b/Source/QuestPDF.Examples/QuestPDF.Examples.csproj
@@ -32,6 +32,9 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/Source/QuestPDF.Examples/multilingual.png b/Source/QuestPDF.Examples/multilingual.png
new file mode 100644
index 0000000..4c07bf0
Binary files /dev/null and b/Source/QuestPDF.Examples/multilingual.png differ
diff --git a/Source/QuestPDF/Drawing/DocumentGenerator.cs b/Source/QuestPDF/Drawing/DocumentGenerator.cs
index 93e5574..696475e 100644
--- a/Source/QuestPDF/Drawing/DocumentGenerator.cs
+++ b/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;
});
}
diff --git a/Source/QuestPDF/Drawing/PdfCanvas.cs b/Source/QuestPDF/Drawing/PdfCanvas.cs
index 380a7c5..af19cfd 100644
--- a/Source/QuestPDF/Drawing/PdfCanvas.cs
+++ b/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
};
}
diff --git a/Source/QuestPDF/Elements/Image.cs b/Source/QuestPDF/Elements/Image.cs
index 74ba5a6..75e70c5 100644
--- a/Source/QuestPDF/Elements/Image.cs
+++ b/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);
}
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF/Fluent/ImageExtensions.cs b/Source/QuestPDF/Fluent/ImageElementExtensions.cs
similarity index 100%
rename from Source/QuestPDF/Fluent/ImageExtensions.cs
rename to Source/QuestPDF/Fluent/ImageElementExtensions.cs
diff --git a/Source/QuestPDF/Fluent/ImageModelExtensions.cs b/Source/QuestPDF/Fluent/ImageModelExtensions.cs
new file mode 100644
index 0000000..df0489c
--- /dev/null
+++ b/Source/QuestPDF/Fluent/ImageModelExtensions.cs
@@ -0,0 +1,62 @@
+using QuestPDF.Drawing.Exceptions;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+ public static class ImageModelExtensions
+ {
+ ///
+ /// When enabled, the image object is disposed automatically after document generation, and you don't need to call the Dispose method yourself.
+ ///
+ public static Image DisposeAfterDocumentGeneration(this Image image)
+ {
+ image.IsDocumentScoped = true;
+ return image;
+ }
+
+ ///
+ /// When enabled, the library will not attempt to resize the image to fit the target DPI, nor save it with target image quality.
+ ///
+ public static Image UseOriginalImage(this Image image)
+ {
+ image.UseOriginalImage = true;
+ return image;
+ }
+
+ ///
+ /// 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.
+ ///
+ public static Image WithRasterDpi(this Image image, int dpi)
+ {
+ image.TargetDpi = dpi;
+ return image;
+ }
+
+ ///
+ /// 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).
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/QuestPDF/Helpers/Helpers.cs b/Source/QuestPDF/Helpers/Helpers.cs
index 668030c..2ba0a3b 100644
--- a/Source/QuestPDF/Helpers/Helpers.cs
+++ b/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)
+ };
+ }
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF/Infrastructure/DocumentSettings.cs b/Source/QuestPDF/Infrastructure/DocumentSettings.cs
index 1419c94..6a4019b 100644
--- a/Source/QuestPDF/Infrastructure/DocumentSettings.cs
+++ b/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).
///
- 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;
///
/// 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.
///
public int RasterDpi { get; set; } = DefaultRasterDpi;
-
+
public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
public static DocumentSettings Default => new DocumentSettings();
diff --git a/Source/QuestPDF/Infrastructure/IDocument.cs b/Source/QuestPDF/Infrastructure/IDocument.cs
index 79dab3f..f149fea 100644
--- a/Source/QuestPDF/Infrastructure/IDocument.cs
+++ b/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
diff --git a/Source/QuestPDF/Infrastructure/Image.cs b/Source/QuestPDF/Infrastructure/Image.cs
index 496ba65..32e4945 100644
--- a/Source/QuestPDF/Infrastructure/Image.cs
+++ b/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
+ }
- ///
- /// When enabled, the image object is disposed automatically after document generation, and you don't need to call the Dispose method yourself.
- ///
- public Image DisposeAfterDocumentGeneration()
- {
- IsDocumentScoped = true;
- return this;
- }
+ #region Scaling Image
- ///
- /// 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.
- ///
- public Image WithRasterDpi(int dpi)
+ internal SKImage GetVersionOfSize(Size availableAreaSize)
{
- TargetDpi = dpi;
- return this;
- }
+ if (UseOriginalImage)
+ return SkImage;
- ///
- /// 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).
- ///
- public Image WithQuality(int value)
- {
- if (value is not (>= 1 and <= 101))
- throw new DocumentComposeException("Image quality must be between 1 and 101.");
+ var imageResolution = new Size(SkImage.Width, SkImage.Height);
+ var targetResolution = GetTargetResolution(ScalingStrategy.Value, imageResolution, availableAreaSize, TargetDpi ?? DocumentSettings.DefaultRasterDpi);
- TargetQuality = value;
- return this;
+ 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;
+
+ return imageResolution;
+ }
+
+ private static SKImage CompressImage(SKImage image, ImageCompressionQuality compressionQuality)
+ {
+ 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);
}
- #endregion
+ private static SKImage ScaleAndCompressImage(SKImage image, Size targetResolution, ImageScalingQuality scalingQuality, ImageCompressionQuality compressionQuality)
+ {
+ var imageInfo = new SKImageInfo((int)targetResolution.Width, (int)targetResolution.Height);
+
+ using var resultImage = SKImage.Create(imageInfo);
+ image.ScalePixels(resultImage.PeekPixels(), scalingQuality.ToFilterQuality());
+
+ return CompressImage(resultImage, compressionQuality);
+ }
+ 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)
diff --git a/Source/QuestPDF/Infrastructure/ImageQuality.cs b/Source/QuestPDF/Infrastructure/ImageCompressionQuality.cs
similarity index 72%
rename from Source/QuestPDF/Infrastructure/ImageQuality.cs
rename to Source/QuestPDF/Infrastructure/ImageCompressionQuality.cs
index 07772e4..b833a62 100644
--- a/Source/QuestPDF/Infrastructure/ImageQuality.cs
+++ b/Source/QuestPDF/Infrastructure/ImageCompressionQuality.cs
@@ -1,40 +1,35 @@
namespace QuestPDF.Infrastructure
{
- public enum ImageQuality
+ public enum ImageCompressionQuality
{
- ///
- /// PNG format with alpha support
- ///
- Lossless = 101,
-
///
/// JPEG format with compression set to 100 out of 100
///
- Max = 100,
+ Best,
///
/// JPEG format with compression set to 90 out of 100
///
- VeryHigh = 90,
+ VeryHigh,
///
/// JPEG format with compression set to 80 out of 100
///
- High = 80,
+ High,
///
/// JPEG format with compression set to 60 out of 100
///
- Medium = 60,
+ Medium,
///
/// JPEG format with compression set to 40 out of 100
///
- Low = 40,
+ Low,
///
/// JPEG format with compression set to 20 out of 100
///
- VeryLow = 20
+ VeryLow
}
}
\ No newline at end of file
diff --git a/Source/QuestPDF/Infrastructure/ImageScalingQuality.cs b/Source/QuestPDF/Infrastructure/ImageScalingQuality.cs
new file mode 100644
index 0000000..7b1e420
--- /dev/null
+++ b/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
+ }
+}
\ No newline at end of file
diff --git a/Source/QuestPDF/Infrastructure/ImageScalingStrategy.cs b/Source/QuestPDF/Infrastructure/ImageScalingStrategy.cs
new file mode 100644
index 0000000..2a873c4
--- /dev/null
+++ b/Source/QuestPDF/Infrastructure/ImageScalingStrategy.cs
@@ -0,0 +1,11 @@
+namespace QuestPDF.Infrastructure
+{
+ public enum ImageScalingStrategy
+ {
+ // TODO: add comments
+ Always,
+ ScaleOnlyToSmallerResolution,
+ ScaleOnlyToSignificantlySmallerResolution,
+ Never
+ }
+}
\ No newline at end of file