diff --git a/Source/QuestPDF.UnitTests/ImageTests.cs b/Source/QuestPDF.UnitTests/ImageTests.cs index 9a7dc1b..6c29a17 100644 --- a/Source/QuestPDF.UnitTests/ImageTests.cs +++ b/Source/QuestPDF.UnitTests/ImageTests.cs @@ -57,13 +57,6 @@ namespace QuestPDF.UnitTests .MeasureElement(new Size(300, 200)) .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] public void UsingSharedImageShouldNotDrasticallyIncreaseDocumentSize() @@ -99,6 +92,25 @@ namespace QuestPDF.UnitTests (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 container) { return Document @@ -112,5 +124,7 @@ namespace QuestPDF.UnitTests .GeneratePdf() .Length; } + + #endregion } } \ No newline at end of file diff --git a/Source/QuestPDF/Drawing/DocumentGenerator.cs b/Source/QuestPDF/Drawing/DocumentGenerator.cs index 2727b15..d3f371f 100644 --- a/Source/QuestPDF/Drawing/DocumentGenerator.cs +++ b/Source/QuestPDF/Drawing/DocumentGenerator.cs @@ -66,7 +66,7 @@ namespace QuestPDF.Drawing var container = new DocumentContainer(); document.Compose(container); var content = container.Compose(); - ApplyDefaultImageDpi(content, metadata.RasterDpi); + ApplyDefaultImageDpi(content, metadata.RasterDpi, metadata.ImageQuality); ApplyDefaultTextStyle(content, TextStyle.LibraryDefault); ApplyContentDirection(content, ContentDirection.LeftToRight); @@ -177,12 +177,15 @@ namespace QuestPDF.Drawing 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 => { - if (x is Image { DocumentImage: { } image }) - image.TargetDpi ??= targetDpi; + if (x is not Image { DocumentImage: { } image }) + return; + + image.TargetDpi ??= targetDpi; + image.ImageQuality ??= targetImageQuality; }); } diff --git a/Source/QuestPDF/Infrastructure/Image.cs b/Source/QuestPDF/Infrastructure/Image.cs index 61898c8..489bcd8 100644 --- a/Source/QuestPDF/Infrastructure/Image.cs +++ b/Source/QuestPDF/Infrastructure/Image.cs @@ -11,20 +11,22 @@ namespace QuestPDF.Infrastructure { private SKImage SkImage { get; } internal List<(Size size, SKImage image)>? ScaledImageCache { 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; } - + public int Width => SkImage.Width; public int Height => SkImage.Height; private const float ImageSizeSimilarityToleranceMax = 1.1f; private const float ImageSizeSimilarityToleranceMin = 1 / ImageSizeSimilarityToleranceMax; - + private Image(SKImage image) { SkImage = image; + PerformScalingToTargetDpi = image.EncodedData.Size >= Settings.AdjustImageSizeThreshold; } public void Dispose() @@ -40,32 +42,60 @@ namespace QuestPDF.Infrastructure var scalingFactor = TargetDpi.Value / (float)DocumentMetadata.DefaultPdfDpi; var targetResolution = new Size(size.Width * scalingFactor, size.Height * scalingFactor); - + if (targetResolution.Width > Width || targetResolution.Height > Height) return SkImage; - + ScaledImageCache ??= new List<(Size size, SKImage image)>(); - + foreach (var imageCache in ScaledImageCache) { if (HasSimilarSize(imageCache.size, targetResolution)) return imageCache.image; } - var scaledImage = ScaleImage(SkImage, targetResolution); + var scaledImage = ScaleImage(SkImage, targetResolution, ImageQuality); ScaledImageCache.Add((targetResolution, scaledImage)); + if (SkImage.EncodedData.Size < scaledImage.EncodedData.Size) + return SkImage; + 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 target = SKImage.Create(imageInfo); + using var target = SKImage.Create(imageInfo); 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) { var widthRatio = a.Width / b.Width; @@ -82,40 +112,58 @@ namespace QuestPDF.Infrastructure { 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 - + #region configuration API - + public Image DisposeAfterDocumentGeneration() { IsDocumentScoped = true; return this; } + /// + /// 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. + /// + /// + /// + 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) { TargetDpi = dpi; diff --git a/Source/QuestPDF/Infrastructure/ImageQuality.cs b/Source/QuestPDF/Infrastructure/ImageQuality.cs new file mode 100644 index 0000000..53f9b98 --- /dev/null +++ b/Source/QuestPDF/Infrastructure/ImageQuality.cs @@ -0,0 +1,40 @@ +namespace QuestPDF.Infrastructure +{ + public enum ImageQuality + { + /// + /// PNG format with alpha support + /// + Lossless = 101, + + /// + /// JPEG format with compression set to 100 out of 100 + /// + Max = 100, + + /// + /// JPEG format with compression set to 90 out of 100 + /// + VeryHigh = 90, + + /// + /// JPEG format with compression set to 80 out of 100 + /// + High = 80, + + /// + /// JPEG format with compression set to 60 out of 100 + /// + Medium = 60, + + /// + /// JPEG format with compression set to 40 out of 100 + /// + Low = 40, + + /// + /// JPEG format with compression set to 20 out of 100 + /// + VeryLow = 20 + } +} \ No newline at end of file diff --git a/Source/QuestPDF/Settings.cs b/Source/QuestPDF/Settings.cs index 002c988..5b298b0 100644 --- a/Source/QuestPDF/Settings.cs +++ b/Source/QuestPDF/Settings.cs @@ -36,5 +36,11 @@ /// /// By default, this flag is enabled only when the debugger IS attached. public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached; + + + /// + /// The file size threshold in bytes that is used to determine if the image should be automatically scaled to physical dimensions. + /// + public static int AdjustImageSizeThreshold { get; set; } = 32 * 1024; } } \ No newline at end of file