Improved image handling
This commit is contained in:
@@ -51,7 +51,7 @@ namespace QuestPDF.UnitTests
|
|||||||
[Test]
|
[Test]
|
||||||
public void Draw_PassesCorrectSizeToSource()
|
public void Draw_PassesCorrectSizeToSource()
|
||||||
{
|
{
|
||||||
Size passedSize = default;
|
ImageSize passedSize = default;
|
||||||
|
|
||||||
TestPlan
|
TestPlan
|
||||||
.For(x => new DynamicImage
|
.For(x => new DynamicImage
|
||||||
@@ -69,9 +69,9 @@ namespace QuestPDF.UnitTests
|
|||||||
passedSize.Should().BeEquivalentTo(new Size(400, 300));
|
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();
|
return image.Encode(SKEncodedImageFormat.Png, 100).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,13 +232,18 @@ namespace QuestPDF.Drawing
|
|||||||
{
|
{
|
||||||
content.VisitChildren(x =>
|
content.VisitChildren(x =>
|
||||||
{
|
{
|
||||||
if (x is not QuestPDF.Elements.Image { DocumentImage: { } image })
|
if (x is QuestPDF.Elements.Image image)
|
||||||
return;
|
{
|
||||||
|
image.TargetDpi ??= settings.RasterDpi;
|
||||||
image.TargetDpi ??= settings.RasterDpi;
|
image.ResizeStrategy ??= settings.ImageResizeStrategy;
|
||||||
image.ScalingQuality ??= settings.ImageScalingQuality;
|
image.CompressionQuality ??= settings.ImageCompressionQuality;
|
||||||
image.ScalingStrategy ??= settings.ImageScalingStrategy;
|
}
|
||||||
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 ContentDirection ContentDirection { get; set; }
|
||||||
|
|
||||||
public float Ratio { get; set; } = 1;
|
public float Ratio { get; set; }
|
||||||
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
|
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
|
||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
|
if (Ratio == 0)
|
||||||
|
return SpacePlan.FullRender(0, 0);
|
||||||
|
|
||||||
if(Child == null)
|
if(Child == null)
|
||||||
return SpacePlan.FullRender(0, 0);
|
return SpacePlan.FullRender(0, 0);
|
||||||
|
|
||||||
@@ -56,6 +59,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
private Size GetTargetSize(Size availableSpace)
|
private Size GetTargetSize(Size availableSpace)
|
||||||
{
|
{
|
||||||
|
if (Ratio == 0)
|
||||||
|
return availableSpace;
|
||||||
|
|
||||||
var spaceRatio = availableSpace.Width / availableSpace.Height;
|
var spaceRatio = availableSpace.Width / availableSpace.Height;
|
||||||
|
|
||||||
var fitHeight = new Size(availableSpace.Height * Ratio, availableSpace.Height) ;
|
var fitHeight = new Size(availableSpace.Height * Ratio, availableSpace.Height) ;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Security;
|
||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
@@ -6,9 +7,13 @@ using SkiaSharp;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
|
public delegate byte[] GenerateDynamicImageDelegate(ImageSize size);
|
||||||
|
|
||||||
internal class DynamicImage : Element
|
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)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
@@ -19,7 +24,8 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
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)
|
if (imageData == null)
|
||||||
return;
|
return;
|
||||||
@@ -27,5 +33,15 @@ namespace QuestPDF.Elements
|
|||||||
using var image = SKImage.FromEncodedData(imageData);
|
using var image = SKImage.FromEncodedData(imageData);
|
||||||
Canvas.DrawImage(image, Position.Zero, availableSpace);
|
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; }
|
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()
|
~Image()
|
||||||
{
|
{
|
||||||
if (DocumentImage is { IsDocumentScoped: true })
|
if (DocumentImage is { IsDocumentScoped: true })
|
||||||
@@ -27,8 +34,62 @@ namespace QuestPDF.Elements
|
|||||||
if (DocumentImage == null)
|
if (DocumentImage == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var image = DocumentImage.GetVersionOfSize(availableSpace);
|
var image = GetImageToDraw(availableSpace);
|
||||||
Canvas.DrawImage(image, Position.Zero, 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 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();
|
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();
|
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();
|
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)
|
if (image == null)
|
||||||
throw new DocumentComposeException("Cannot load or decode provided image.");
|
throw new DocumentComposeException("Cannot load or decode provided image.");
|
||||||
@@ -37,32 +37,64 @@ namespace QuestPDF.Fluent
|
|||||||
DocumentImage = image
|
DocumentImage = image
|
||||||
};
|
};
|
||||||
|
|
||||||
if (scaling != ImageScaling.Resize)
|
var aspectRationElement = new AspectRatio
|
||||||
{
|
{
|
||||||
var aspectRatio = image.Width / (float)image.Height;
|
Child = imageElement
|
||||||
parent = parent.AspectRatio(aspectRatio, Map(scaling));
|
};
|
||||||
}
|
|
||||||
|
parent.Element(aspectRationElement);
|
||||||
parent.Element(imageElement);
|
return new ImageDescriptor(imageElement, aspectRationElement).FitWidth();
|
||||||
|
|
||||||
static AspectRatioOption Map(ImageScaling scaling)
|
|
||||||
{
|
|
||||||
return scaling switch
|
|
||||||
{
|
|
||||||
ImageScaling.FitWidth => AspectRatioOption.FitWidth,
|
|
||||||
ImageScaling.FitHeight => AspectRatioOption.FitHeight,
|
|
||||||
ImageScaling.FitArea => AspectRatioOption.FitArea,
|
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Image(this IContainer element, Func<Size, byte[]> imageSource)
|
public static void Image(this IContainer element, GenerateDynamicImageDelegate dynamicImageSource)
|
||||||
{
|
{
|
||||||
element.Element(new DynamicImage
|
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.Drawing.Exceptions;
|
||||||
|
using QuestPDF.Elements;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Fluent
|
namespace QuestPDF.Fluent
|
||||||
{
|
{
|
||||||
public static class ImageModelExtensions
|
public class ImageDescriptor
|
||||||
{
|
{
|
||||||
/// <summary>
|
private Elements.Image ImageElement { get; }
|
||||||
/// When enabled, the image object is disposed automatically after document generation, and you don't need to call the Dispose method yourself.
|
private AspectRatio AspectRatioElement { get; }
|
||||||
/// </summary>
|
private float ImageAspectRatio { get; }
|
||||||
public static Image DisposeAfterDocumentGeneration(this Image image)
|
|
||||||
{
|
|
||||||
image.IsDocumentScoped = true;
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
internal ImageDescriptor(Elements.Image imageElement, Elements.AspectRatio aspectRatioElement)
|
||||||
|
{
|
||||||
|
ImageElement = imageElement;
|
||||||
|
AspectRatioElement = aspectRatioElement;
|
||||||
|
|
||||||
|
var imageSize = ImageElement.DocumentImage.Size;
|
||||||
|
ImageAspectRatio = imageSize.Width / (float)imageSize.Height;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When enabled, the library will not attempt to resize the image to fit the target DPI, nor save it with target image quality.
|
/// When enabled, the library will not attempt to resize the image to fit the target DPI, nor save it with target image quality.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Image UseOriginalImage(this Image image)
|
public ImageDescriptor UseOriginalImage(bool value = true)
|
||||||
{
|
{
|
||||||
image.UseOriginalImage = true;
|
ImageElement.UseOriginalImage = value;
|
||||||
return image;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -29,10 +34,10 @@ namespace QuestPDF.Fluent
|
|||||||
/// When generating images, this parameter also controls the resolution of the generated content.
|
/// When generating images, this parameter also controls the resolution of the generated content.
|
||||||
/// Default value is 72.
|
/// Default value is 72.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Image WithRasterDpi(this Image image, int dpi)
|
public ImageDescriptor WithRasterDpi(int dpi)
|
||||||
{
|
{
|
||||||
image.TargetDpi = dpi;
|
ImageElement.TargetDpi = dpi;
|
||||||
return image;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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).
|
/// The default value is 90 (very high quality).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Image WithCompressionQuality(this Image image, ImageCompressionQuality quality)
|
public ImageDescriptor WithCompressionQuality(ImageCompressionQuality quality)
|
||||||
{
|
{
|
||||||
image.CompressionQuality = quality;
|
ImageElement.CompressionQuality = quality;
|
||||||
return image;
|
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 SetAspectRatio(AspectRatioOption.FitWidth);
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Image WithScalingStrategy(this Image image, ImageScalingStrategy strategy)
|
public ImageDescriptor FitHeight()
|
||||||
{
|
{
|
||||||
image.ScalingStrategy = strategy;
|
return SetAspectRatio(AspectRatioOption.FitHeight);
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
_ => 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)
|
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
|
// shuffle corner positions
|
||||||
var targetPositions = new[]
|
var targetPositions = new[]
|
||||||
@@ -239,7 +239,7 @@ namespace QuestPDF.Helpers
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// create image with white background
|
// 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 surface = SKSurface.Create(imageInfo);
|
||||||
|
|
||||||
using var backgroundPaint = new SKPaint
|
using var backgroundPaint = new SKPaint
|
||||||
|
|||||||
@@ -20,9 +20,7 @@
|
|||||||
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
|
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
|
||||||
|
|
||||||
// TODO: add comments
|
// TODO: add comments
|
||||||
public ImageScalingStrategy ImageScalingStrategy { get; set; } = ImageScalingStrategy.ScaleOnlyToSignificantlySmallerResolution;
|
public ImageResizeStrategy ImageResizeStrategy { get; set; } = ImageResizeStrategy.ScaleOnlyToSignificantlySmallerResolution;
|
||||||
|
|
||||||
public ImageScalingQuality ImageScalingQuality { get; set; } = ImageScalingQuality.High;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The DPI (pixels-per-inch) at which images and features without native PDF support will be rasterized.
|
/// 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using QuestPDF.Drawing.Exceptions;
|
using QuestPDF.Drawing.Exceptions;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
@@ -8,75 +7,55 @@ using SkiaSharp;
|
|||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
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
|
public class Image : IDisposable
|
||||||
{
|
{
|
||||||
private SKImage SkImage { get; }
|
internal SKImage SkImage { get; }
|
||||||
public int Width => SkImage.Width;
|
internal ImageSize Size { get; }
|
||||||
public int Height => SkImage.Height;
|
|
||||||
|
|
||||||
internal List<(Size size, SKImage image)>? ScaledImageCache { get; set; }
|
|
||||||
|
|
||||||
internal bool IsDocumentScoped { 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;
|
|
||||||
|
|
||||||
|
internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
|
||||||
|
|
||||||
private Image(SKImage image)
|
private Image(SKImage image)
|
||||||
{
|
{
|
||||||
SkImage = image;
|
SkImage = image;
|
||||||
|
Size = new ImageSize(image.Width, image.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SkImage.Dispose();
|
SkImage.Dispose();
|
||||||
|
|
||||||
|
foreach (var cacheKey in ScaledImageCache)
|
||||||
|
cacheKey.image.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Scaling Image
|
#region Scaling Image
|
||||||
|
|
||||||
internal SKImage GetVersionOfSize(Size availableAreaSize)
|
internal SKImage GetVersionOfSize(GetImageVersionRequest request)
|
||||||
{
|
{
|
||||||
if (UseOriginalImage)
|
foreach (var cacheKey in ScaledImageCache)
|
||||||
return SkImage;
|
{
|
||||||
|
if (cacheKey.request == request)
|
||||||
|
return cacheKey.image;
|
||||||
|
}
|
||||||
|
|
||||||
var imageResolution = new Size(SkImage.Width, SkImage.Height);
|
var result = ResizeAndCompressImage(SkImage, request.Resolution, request.CompressionQuality);
|
||||||
var targetResolution = GetTargetResolution(ScalingStrategy.Value, imageResolution, availableAreaSize, TargetDpi ?? DocumentSettings.DefaultRasterDpi);
|
ScaledImageCache.AddLast((request, result));
|
||||||
|
return result;
|
||||||
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)
|
private static SKImage CompressImage(SKImage image, ImageCompressionQuality compressionQuality)
|
||||||
{
|
{
|
||||||
var targetFormat = image.Info.IsOpaque
|
var targetFormat = image.Info.IsOpaque
|
||||||
? SKEncodedImageFormat.Png
|
? SKEncodedImageFormat.Jpeg
|
||||||
: SKEncodedImageFormat.Jpeg;
|
: SKEncodedImageFormat.Png;
|
||||||
|
|
||||||
if (targetFormat == SKEncodedImageFormat.Png)
|
if (targetFormat == SKEncodedImageFormat.Png)
|
||||||
compressionQuality = ImageCompressionQuality.Best;
|
compressionQuality = ImageCompressionQuality.Best;
|
||||||
@@ -85,29 +64,19 @@ namespace QuestPDF.Infrastructure
|
|||||||
return SKImage.FromEncodedData(data);
|
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);
|
using var resultImage = SKImage.Create(imageInfo);
|
||||||
image.ScalePixels(resultImage.PeekPixels(), scalingQuality.ToFilterQuality());
|
image.ScalePixels(resultImage.PeekPixels(), SKFilterQuality.Medium);
|
||||||
|
|
||||||
return CompressImage(resultImage, compressionQuality);
|
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
|
#endregion
|
||||||
|
|
||||||
#region public constructors
|
#region public constructors
|
||||||
@@ -141,5 +110,11 @@ namespace QuestPDF.Infrastructure
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
internal Image DisposeAfterDocumentGeneration()
|
||||||
|
{
|
||||||
|
IsDocumentScoped = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
{
|
{
|
||||||
public enum ImageScalingStrategy
|
public enum ImageResizeStrategy
|
||||||
{
|
{
|
||||||
// TODO: add comments
|
// TODO: add comments
|
||||||
Always,
|
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