Improved image handling
This commit is contained in:
@@ -51,7 +51,7 @@ namespace QuestPDF.UnitTests
|
||||
[Test]
|
||||
public void Draw_PassesCorrectSizeToSource()
|
||||
{
|
||||
Size passedSize = default;
|
||||
ImageSize passedSize = default;
|
||||
|
||||
TestPlan
|
||||
.For(x => new DynamicImage
|
||||
@@ -69,9 +69,9 @@ namespace QuestPDF.UnitTests
|
||||
passedSize.Should().BeEquivalentTo(new Size(400, 300));
|
||||
}
|
||||
|
||||
byte[] GenerateImage(Size size)
|
||||
byte[] GenerateImage(ImageSize size)
|
||||
{
|
||||
var image = GenerateImage((int) size.Width, (int) size.Height);
|
||||
var image = GenerateImage(size.Width, size.Height);
|
||||
return image.Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -232,13 +232,18 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
content.VisitChildren(x =>
|
||||
{
|
||||
if (x is not QuestPDF.Elements.Image { DocumentImage: { } image })
|
||||
return;
|
||||
|
||||
image.TargetDpi ??= settings.RasterDpi;
|
||||
image.ScalingQuality ??= settings.ImageScalingQuality;
|
||||
image.ScalingStrategy ??= settings.ImageScalingStrategy;
|
||||
image.CompressionQuality ??= settings.ImageCompressionQuality;
|
||||
if (x is QuestPDF.Elements.Image image)
|
||||
{
|
||||
image.TargetDpi ??= settings.RasterDpi;
|
||||
image.ResizeStrategy ??= settings.ImageResizeStrategy;
|
||||
image.CompressionQuality ??= settings.ImageCompressionQuality;
|
||||
}
|
||||
|
||||
if (x is QuestPDF.Elements.DynamicImage dynamicImage)
|
||||
{
|
||||
dynamicImage.TargetDpi ??= settings.RasterDpi;
|
||||
dynamicImage.CompressionQuality ??= settings.ImageCompressionQuality;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,11 +8,14 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
public ContentDirection ContentDirection { get; set; }
|
||||
|
||||
public float Ratio { get; set; } = 1;
|
||||
public float Ratio { get; set; }
|
||||
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (Ratio == 0)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
if(Child == null)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
@@ -56,6 +59,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
private Size GetTargetSize(Size availableSpace)
|
||||
{
|
||||
if (Ratio == 0)
|
||||
return availableSpace;
|
||||
|
||||
var spaceRatio = availableSpace.Width / availableSpace.Height;
|
||||
|
||||
var fitHeight = new Size(availableSpace.Height * Ratio, availableSpace.Height) ;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
@@ -6,9 +7,13 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
public delegate byte[] GenerateDynamicImageDelegate(ImageSize size);
|
||||
|
||||
internal class DynamicImage : Element
|
||||
{
|
||||
public Func<Size, byte[]>? Source { get; set; }
|
||||
internal int? TargetDpi { get; set; }
|
||||
internal ImageCompressionQuality? CompressionQuality { get; set; }
|
||||
public GenerateDynamicImageDelegate? Source { get; set; }
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
@@ -19,7 +24,8 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
var imageData = Source?.Invoke(availableSpace);
|
||||
var targetResolution = GetTargetResolution(availableSpace, TargetDpi.Value);
|
||||
var imageData = Source?.Invoke(targetResolution);
|
||||
|
||||
if (imageData == null)
|
||||
return;
|
||||
@@ -27,5 +33,15 @@ namespace QuestPDF.Elements
|
||||
using var image = SKImage.FromEncodedData(imageData);
|
||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
||||
}
|
||||
|
||||
private static ImageSize GetTargetResolution(Size availableSize, int targetDpi)
|
||||
{
|
||||
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
|
||||
|
||||
return new ImageSize(
|
||||
(int)(availableSize.Width * scalingFactor),
|
||||
(int)(availableSize.Height * scalingFactor)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,13 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
public Infrastructure.Image? DocumentImage { get; set; }
|
||||
|
||||
internal bool UseOriginalImage { get; set; }
|
||||
internal int? TargetDpi { get; set; }
|
||||
internal ImageCompressionQuality? CompressionQuality { get; set; }
|
||||
internal ImageResizeStrategy? ResizeStrategy { get; set; }
|
||||
|
||||
private const float ImageSizeSimilarityToleranceMax = 0.75f;
|
||||
|
||||
~Image()
|
||||
{
|
||||
if (DocumentImage is { IsDocumentScoped: true })
|
||||
@@ -27,8 +34,62 @@ namespace QuestPDF.Elements
|
||||
if (DocumentImage == null)
|
||||
return;
|
||||
|
||||
var image = DocumentImage.GetVersionOfSize(availableSpace);
|
||||
var image = GetImageToDraw(availableSpace);
|
||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
||||
}
|
||||
|
||||
private SKImage GetImageToDraw(Size availableSpace)
|
||||
{
|
||||
if (UseOriginalImage)
|
||||
return DocumentImage.SkImage;
|
||||
|
||||
var request = new GetImageVersionRequest
|
||||
{
|
||||
Resolution = GetTargetResolution(ResizeStrategy.Value, DocumentImage.Size, availableSpace, TargetDpi.Value),
|
||||
CompressionQuality = CompressionQuality.Value,
|
||||
ResizeStrategy = ResizeStrategy.Value
|
||||
};
|
||||
|
||||
return DocumentImage.GetVersionOfSize(request);
|
||||
}
|
||||
|
||||
private static ImageSize GetTargetResolution(ImageResizeStrategy resizeStrategy, ImageSize imageResolution, Size availableAreaSize, int targetDpi)
|
||||
{
|
||||
if (resizeStrategy == ImageResizeStrategy.Never)
|
||||
return imageResolution;
|
||||
|
||||
var scalingFactor = targetDpi / (float)DocumentSettings.DefaultRasterDpi;
|
||||
var targetResolution = new ImageSize(
|
||||
(int)(availableAreaSize.Width * scalingFactor),
|
||||
(int)(availableAreaSize.Height * scalingFactor));
|
||||
|
||||
if (resizeStrategy == ImageResizeStrategy.Always)
|
||||
return targetResolution;
|
||||
|
||||
var isSignificantlySmaller = IsImageSignificantlySmallerThanDrawingArea(imageResolution, targetResolution);
|
||||
|
||||
if (resizeStrategy == ImageResizeStrategy.ScaleOnlyToSignificantlySmallerResolution && isSignificantlySmaller)
|
||||
return targetResolution;
|
||||
|
||||
var isSmaller = IsImageSmallerThanDrawingArea(imageResolution, targetResolution);
|
||||
|
||||
if (resizeStrategy == ImageResizeStrategy.ScaleOnlyToSmallerResolution && isSmaller)
|
||||
return targetResolution;
|
||||
|
||||
return imageResolution;
|
||||
}
|
||||
|
||||
private static bool IsImageSmallerThanDrawingArea(ImageSize imageResolution, ImageSize targetResolution)
|
||||
{
|
||||
return imageResolution.Width < targetResolution.Width || imageResolution.Height < targetResolution.Height;
|
||||
}
|
||||
|
||||
private static bool IsImageSignificantlySmallerThanDrawingArea(ImageSize imageResolution, ImageSize targetResolution, float sizeSimilarityThreshold = ImageSizeSimilarityToleranceMax)
|
||||
{
|
||||
var widthRatio = targetResolution.Width / imageResolution.Width;
|
||||
var heightRatio = targetResolution.Height / imageResolution.Height;
|
||||
|
||||
return widthRatio < sizeSimilarityThreshold && heightRatio < sizeSimilarityThreshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,25 +9,25 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class ImageExtensions
|
||||
{
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, byte[] imageData)
|
||||
{
|
||||
var image = Infrastructure.Image.FromBinaryData(imageData).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, string filePath)
|
||||
{
|
||||
var image = Infrastructure.Image.FromFile(filePath).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
public static ImageDescriptor Image(this IContainer parent, Stream fileStream)
|
||||
{
|
||||
var image = Infrastructure.Image.FromStream(fileStream).DisposeAfterDocumentGeneration();
|
||||
parent.Image(image, scaling);
|
||||
return parent.Image(image);
|
||||
}
|
||||
|
||||
internal static void Image(this IContainer parent, Infrastructure.Image image, ImageScaling scaling = ImageScaling.FitWidth)
|
||||
internal static ImageDescriptor Image(this IContainer parent, Infrastructure.Image image)
|
||||
{
|
||||
if (image == null)
|
||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||
@@ -37,32 +37,64 @@ namespace QuestPDF.Fluent
|
||||
DocumentImage = image
|
||||
};
|
||||
|
||||
if (scaling != ImageScaling.Resize)
|
||||
var aspectRationElement = new AspectRatio
|
||||
{
|
||||
var aspectRatio = image.Width / (float)image.Height;
|
||||
parent = parent.AspectRatio(aspectRatio, Map(scaling));
|
||||
}
|
||||
|
||||
parent.Element(imageElement);
|
||||
|
||||
static AspectRatioOption Map(ImageScaling scaling)
|
||||
{
|
||||
return scaling switch
|
||||
{
|
||||
ImageScaling.FitWidth => AspectRatioOption.FitWidth,
|
||||
ImageScaling.FitHeight => AspectRatioOption.FitHeight,
|
||||
ImageScaling.FitArea => AspectRatioOption.FitArea,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
Child = imageElement
|
||||
};
|
||||
|
||||
parent.Element(aspectRationElement);
|
||||
return new ImageDescriptor(imageElement, aspectRationElement).FitWidth();
|
||||
}
|
||||
|
||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
||||
public static void Image(this IContainer element, GenerateDynamicImageDelegate dynamicImageSource)
|
||||
{
|
||||
element.Element(new DynamicImage
|
||||
{
|
||||
Source = imageSource
|
||||
Source = dynamicImageSource
|
||||
});
|
||||
}
|
||||
|
||||
#region Obsolete
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that takes the GenerateDynamicImageDelegate as an argument.")]
|
||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
||||
{
|
||||
element.Image((ImageSize x) => imageSource(new Size(x.Width, x.Height)));
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(imageData).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, string filePath, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(filePath).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been changed since version 2023.5. Please use the Image method overload that returns the ImageDescriptor object.")]
|
||||
public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling)
|
||||
{
|
||||
parent.Image(fileStream).ApplyScaling(scaling);
|
||||
}
|
||||
|
||||
internal static void ApplyScaling(this ImageDescriptor descriptor, ImageScaling scaling)
|
||||
{
|
||||
if (scaling == ImageScaling.Resize)
|
||||
descriptor.FitUnproportionally();
|
||||
|
||||
else if (scaling == ImageScaling.FitWidth)
|
||||
descriptor.FitWidth();
|
||||
|
||||
else if (scaling == ImageScaling.FitHeight)
|
||||
descriptor.FitHeight();
|
||||
|
||||
else if (scaling == ImageScaling.FitArea)
|
||||
descriptor.FitArea();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,31 @@
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class ImageModelExtensions
|
||||
public class ImageDescriptor
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
private Elements.Image ImageElement { get; }
|
||||
private AspectRatio AspectRatioElement { get; }
|
||||
private float ImageAspectRatio { get; }
|
||||
|
||||
internal ImageDescriptor(Elements.Image imageElement, Elements.AspectRatio aspectRatioElement)
|
||||
{
|
||||
ImageElement = imageElement;
|
||||
AspectRatioElement = aspectRatioElement;
|
||||
|
||||
var imageSize = ImageElement.DocumentImage.Size;
|
||||
ImageAspectRatio = imageSize.Width / (float)imageSize.Height;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
public ImageDescriptor UseOriginalImage(bool value = true)
|
||||
{
|
||||
image.UseOriginalImage = true;
|
||||
return image;
|
||||
ImageElement.UseOriginalImage = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -29,10 +34,10 @@ namespace QuestPDF.Fluent
|
||||
/// 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)
|
||||
public ImageDescriptor WithRasterDpi(int dpi)
|
||||
{
|
||||
image.TargetDpi = dpi;
|
||||
return image;
|
||||
ImageElement.TargetDpi = dpi;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -41,22 +46,48 @@ namespace QuestPDF.Fluent
|
||||
/// 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)
|
||||
public ImageDescriptor WithCompressionQuality(ImageCompressionQuality quality)
|
||||
{
|
||||
image.CompressionQuality = quality;
|
||||
return image;
|
||||
ImageElement.CompressionQuality = quality;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageDescriptor WithResizeStrategy(ImageResizeStrategy strategy)
|
||||
{
|
||||
ImageElement.ResizeStrategy = strategy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public static Image WithScalingQuality(this Image image, ImageScalingQuality strategy)
|
||||
#region Aspect Ratio
|
||||
|
||||
public ImageDescriptor FitWidth()
|
||||
{
|
||||
image.ScalingQuality = strategy;
|
||||
return image;
|
||||
return SetAspectRatio(AspectRatioOption.FitWidth);
|
||||
}
|
||||
|
||||
public static Image WithScalingStrategy(this Image image, ImageScalingStrategy strategy)
|
||||
public ImageDescriptor FitHeight()
|
||||
{
|
||||
image.ScalingStrategy = strategy;
|
||||
return image;
|
||||
return SetAspectRatio(AspectRatioOption.FitHeight);
|
||||
}
|
||||
|
||||
public ImageDescriptor FitArea()
|
||||
{
|
||||
return SetAspectRatio(AspectRatioOption.FitArea);
|
||||
}
|
||||
|
||||
public ImageDescriptor FitUnproportionally()
|
||||
{
|
||||
AspectRatioElement.Ratio = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
private ImageDescriptor SetAspectRatio(AspectRatioOption option)
|
||||
{
|
||||
AspectRatioElement.Ratio = ImageAspectRatio;
|
||||
AspectRatioElement.Option = option;
|
||||
return this;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -75,17 +75,5 @@ namespace QuestPDF.Helpers
|
||||
_ => 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,10 +213,10 @@ namespace QuestPDF.Helpers
|
||||
|
||||
public static byte[] Image(int width, int height)
|
||||
{
|
||||
return Image(new Size(width, height));
|
||||
return Image(new ImageSize(width, height));
|
||||
}
|
||||
|
||||
public static byte[] Image(Size size)
|
||||
public static byte[] Image(ImageSize size)
|
||||
{
|
||||
// shuffle corner positions
|
||||
var targetPositions = new[]
|
||||
@@ -239,7 +239,7 @@ namespace QuestPDF.Helpers
|
||||
.ToArray();
|
||||
|
||||
// create image with white background
|
||||
var imageInfo = new SKImageInfo((int)size.Width, (int)size.Height);
|
||||
var imageInfo = new SKImageInfo(size.Width, size.Height);
|
||||
using var surface = SKSurface.Create(imageInfo);
|
||||
|
||||
using var backgroundPaint = new SKPaint
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
|
||||
|
||||
// TODO: add comments
|
||||
public ImageScalingStrategy ImageScalingStrategy { get; set; } = ImageScalingStrategy.ScaleOnlyToSignificantlySmallerResolution;
|
||||
|
||||
public ImageScalingQuality ImageScalingQuality { get; set; } = ImageScalingQuality.High;
|
||||
public ImageResizeStrategy ImageResizeStrategy { get; set; } = ImageResizeStrategy.ScaleOnlyToSignificantlySmallerResolution;
|
||||
|
||||
/// <summary>
|
||||
/// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Helpers;
|
||||
@@ -8,75 +7,55 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal record GetImageVersionRequest
|
||||
{
|
||||
internal ImageSize Resolution { get; set; }
|
||||
internal ImageCompressionQuality CompressionQuality { get; set; }
|
||||
internal ImageResizeStrategy ResizeStrategy { get; set; }
|
||||
}
|
||||
|
||||
public class Image : IDisposable
|
||||
{
|
||||
private SKImage SkImage { get; }
|
||||
public int Width => SkImage.Width;
|
||||
public int Height => SkImage.Height;
|
||||
|
||||
internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
|
||||
|
||||
internal SKImage SkImage { get; }
|
||||
internal ImageSize Size { get; }
|
||||
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;
|
||||
|
||||
internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
|
||||
|
||||
private Image(SKImage image)
|
||||
{
|
||||
SkImage = image;
|
||||
Size = new ImageSize(image.Width, image.Height);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SkImage.Dispose();
|
||||
|
||||
foreach (var cacheKey in ScaledImageCache)
|
||||
cacheKey.image.Dispose();
|
||||
}
|
||||
|
||||
#region Scaling Image
|
||||
|
||||
internal SKImage GetVersionOfSize(Size availableAreaSize)
|
||||
internal SKImage GetVersionOfSize(GetImageVersionRequest request)
|
||||
{
|
||||
if (UseOriginalImage)
|
||||
return SkImage;
|
||||
foreach (var cacheKey in ScaledImageCache)
|
||||
{
|
||||
if (cacheKey.request == request)
|
||||
return cacheKey.image;
|
||||
}
|
||||
|
||||
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);
|
||||
var result = ResizeAndCompressImage(SkImage, request.Resolution, request.CompressionQuality);
|
||||
ScaledImageCache.AddLast((request, result));
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
? SKEncodedImageFormat.Jpeg
|
||||
: SKEncodedImageFormat.Png;
|
||||
|
||||
if (targetFormat == SKEncodedImageFormat.Png)
|
||||
compressionQuality = ImageCompressionQuality.Best;
|
||||
@@ -85,29 +64,19 @@ namespace QuestPDF.Infrastructure
|
||||
return SKImage.FromEncodedData(data);
|
||||
}
|
||||
|
||||
private static SKImage ScaleAndCompressImage(SKImage image, Size targetResolution, ImageScalingQuality scalingQuality, ImageCompressionQuality compressionQuality)
|
||||
private static SKImage ResizeAndCompressImage(SKImage image, ImageSize targetResolution, ImageCompressionQuality compressionQuality)
|
||||
{
|
||||
var imageInfo = new SKImageInfo((int)targetResolution.Width, (int)targetResolution.Height);
|
||||
if (image.Width == targetResolution.Width && image.Height == targetResolution.Height)
|
||||
return CompressImage(image, compressionQuality);
|
||||
|
||||
var imageInfo = new SKImageInfo(targetResolution.Width, targetResolution.Height);
|
||||
|
||||
using var resultImage = SKImage.Create(imageInfo);
|
||||
image.ScalePixels(resultImage.PeekPixels(), scalingQuality.ToFilterQuality());
|
||||
image.ScalePixels(resultImage.PeekPixels(), SKFilterQuality.Medium);
|
||||
|
||||
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
|
||||
@@ -141,5 +110,11 @@ namespace QuestPDF.Infrastructure
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal Image DisposeAfterDocumentGeneration()
|
||||
{
|
||||
IsDocumentScoped = true;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public enum ImageScalingStrategy
|
||||
public enum ImageResizeStrategy
|
||||
{
|
||||
// TODO: add comments
|
||||
Always,
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
// https://chromium.googlesource.com/skia/+/master/include/core/SkFilterQuality.h
|
||||
public enum ImageScalingQuality
|
||||
{
|
||||
// TODO: add comments
|
||||
Low,
|
||||
|
||||
Medium,
|
||||
|
||||
High,
|
||||
|
||||
Best
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public struct ImageSize
|
||||
{
|
||||
public readonly int Width;
|
||||
public readonly int Height;
|
||||
|
||||
public ImageSize(int width, int height)
|
||||
{
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user