Implemented automated image scaling

This commit is contained in:
MarcinZiabek
2023-05-01 14:23:46 +02:00
parent 9637dff1dc
commit e53b586943
5 changed files with 142 additions and 31 deletions
+21 -7
View File
@@ -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<IContainer> container)
{
return Document
@@ -112,5 +124,7 @@ namespace QuestPDF.UnitTests
.GeneratePdf()
.Length;
}
#endregion
}
}
+7 -4
View File
@@ -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;
});
}
+68 -20
View File
@@ -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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="quality"></param>
/// <returns></returns>
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;
@@ -0,0 +1,40 @@
namespace QuestPDF.Infrastructure
{
public enum ImageQuality
{
/// <summary>
/// PNG format with alpha support
/// </summary>
Lossless = 101,
/// <summary>
/// JPEG format with compression set to 100 out of 100
/// </summary>
Max = 100,
/// <summary>
/// JPEG format with compression set to 90 out of 100
/// </summary>
VeryHigh = 90,
/// <summary>
/// JPEG format with compression set to 80 out of 100
/// </summary>
High = 80,
/// <summary>
/// JPEG format with compression set to 60 out of 100
/// </summary>
Medium = 60,
/// <summary>
/// JPEG format with compression set to 40 out of 100
/// </summary>
Low = 40,
/// <summary>
/// JPEG format with compression set to 20 out of 100
/// </summary>
VeryLow = 20
}
}
+6
View File
@@ -36,5 +36,11 @@
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
/// <summary>
/// The file size threshold in bytes that is used to determine if the image should be automatically scaled to physical dimensions.
/// </summary>
public static int AdjustImageSizeThreshold { get; set; } = 32 * 1024;
}
}