Integrated new Image API
This commit is contained in:
@@ -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()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,9 @@
|
||||
<None Update="pdf-icon.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="multilingual.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace QuestPDF.Drawing
|
||||
Modified = metadata.ModifiedDate,
|
||||
|
||||
RasterDpi = documentSettings.RasterDpi,
|
||||
EncodingQuality = documentSettings.ImageQuality,
|
||||
EncodingQuality = documentSettings.ImageCompressionQuality.ToQualityValue(),
|
||||
PdfA = documentSettings.PdfA
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class ImageModelExtensions
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
image.UseOriginalImage = true;
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static Image WithRasterDpi(this Image image, int dpi)
|
||||
{
|
||||
image.TargetDpi = dpi;
|
||||
return image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public int RasterDpi { get; set; } = DefaultRasterDpi;
|
||||
|
||||
|
||||
public ContentDirection ContentDirection { get; set; } = ContentDirection.LeftToRight;
|
||||
|
||||
public static DocumentSettings Default => new DocumentSettings();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <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 Image DisposeAfterDocumentGeneration()
|
||||
{
|
||||
IsDocumentScoped = true;
|
||||
return this;
|
||||
}
|
||||
#region Scaling Image
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public Image WithRasterDpi(int dpi)
|
||||
internal SKImage GetVersionOfSize(Size availableAreaSize)
|
||||
{
|
||||
TargetDpi = dpi;
|
||||
return this;
|
||||
}
|
||||
if (UseOriginalImage)
|
||||
return SkImage;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
+7
-12
@@ -1,40 +1,35 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public enum ImageQuality
|
||||
public enum ImageCompressionQuality
|
||||
{
|
||||
/// <summary>
|
||||
/// PNG format with alpha support
|
||||
/// </summary>
|
||||
Lossless = 101,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 100 out of 100
|
||||
/// </summary>
|
||||
Max = 100,
|
||||
Best,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 90 out of 100
|
||||
/// </summary>
|
||||
VeryHigh = 90,
|
||||
VeryHigh,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 80 out of 100
|
||||
/// </summary>
|
||||
High = 80,
|
||||
High,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 60 out of 100
|
||||
/// </summary>
|
||||
Medium = 60,
|
||||
Medium,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 40 out of 100
|
||||
/// </summary>
|
||||
Low = 40,
|
||||
Low,
|
||||
|
||||
/// <summary>
|
||||
/// JPEG format with compression set to 20 out of 100
|
||||
/// </summary>
|
||||
VeryLow = 20
|
||||
VeryLow
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public enum ImageScalingStrategy
|
||||
{
|
||||
// TODO: add comments
|
||||
Always,
|
||||
ScaleOnlyToSmallerResolution,
|
||||
ScaleOnlyToSignificantlySmallerResolution,
|
||||
Never
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user