Compare commits

...

7 Commits

Author SHA1 Message Date
Marcin Ziąbek f67b65e040 Merge branch 'main' into feature/image-generation-settings 2023-05-20 12:00:18 +02:00
MarcinZiabek c5f77e1fd1 Hidden SkiaSharp API 2023-05-20 02:10:34 +02:00
MarcinZiabek f60aea0f83 Image generation: improved performance and quality 2023-05-20 01:52:05 +02:00
MarcinZiabek b74e0327fa Improved SharedImage API 2023-05-20 01:16:24 +02:00
swcs f20d7af0c5 new overloads to generate images based on the passed settings 2023-05-17 16:09:57 +02:00
swcs 3a73886d70 ImageCanvas uses on ImageGenerationSettings now 2023-05-17 16:09:41 +02:00
swcs 045b2a43b8 Settings for the GenerateImages() method 2023-05-17 16:08:47 +02:00
14 changed files with 182 additions and 39 deletions
+34 -5
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Drawing.Exceptions; using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
@@ -79,11 +80,11 @@ namespace QuestPDF.Examples
.ShowResults() .ShowResults()
.Render(page => .Render(page =>
{ {
page.Padding(10).Column(column => page.Padding(15).Column(column =>
{ {
column.Spacing(10); column.Spacing(15);
column.Item().Image("photo.jpg").WithRasterDpi(16); column.Item().Image("photo.jpg").WithRasterDpi(16).FitUnproportionally();
column.Item().Image("photo.jpg").WithRasterDpi(72); column.Item().Image("photo.jpg").WithRasterDpi(72);
}); });
}); });
@@ -99,9 +100,9 @@ namespace QuestPDF.Examples
.ShowResults() .ShowResults()
.Render(page => .Render(page =>
{ {
page.Padding(10).Column(column => page.Padding(15).Column(column =>
{ {
column.Spacing(10); column.Spacing(15);
column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72); column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.VeryLow).WithRasterDpi(72);
column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.High).WithRasterDpi(72); column.Item().Image("photo.jpg").WithCompressionQuality(ImageCompressionQuality.High).WithRasterDpi(72);
@@ -109,6 +110,34 @@ namespace QuestPDF.Examples
}); });
} }
[Test]
public void ReusingImage_With()
{
RenderingTest
.Create()
.PageSize(400, 600)
.ProduceImages()
.ShowResults()
.Render(page =>
{
page.Padding(15).Column(column =>
{
column.Spacing(15);
var image = Image.FromFile("checkbox.png");
foreach (var i in Enumerable.Range(0, 5))
{
column.Item().Row(row =>
{
row.AutoItem().Width(24).Image(image);
row.RelativeItem().PaddingLeft(8).AlignMiddle().Text(Placeholders.Label()).FontSize(16);
});
}
});
});
}
[Test] [Test]
public void Exception() public void Exception()
{ {
@@ -38,6 +38,9 @@
<None Update="photo.jpg"> <None Update="photo.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="checkbox.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
</Project> </Project>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+1 -1
View File
@@ -85,7 +85,7 @@ namespace QuestPDF.UnitTests
{ {
container.Column(column => container.Column(column =>
{ {
var sharedImage = DocumentImage.FromBinaryData(photo).DisposeAfterDocumentGeneration(); var sharedImage = DocumentImage.FromBinaryData(photo);
foreach (var i in Enumerable.Range(0, 10)) foreach (var i in Enumerable.Range(0, 10))
column.Item().Image(sharedImage); column.Item().Image(sharedImage);
+11 -7
View File
@@ -45,13 +45,14 @@ namespace QuestPDF.Drawing
throw new ArgumentException("The library requires a Stream object with the 'seek' capability available (the CanSeek flag). Please consider using the MemoryStream class."); throw new ArgumentException("The library requires a Stream object with the 'seek' capability available (the CanSeek flag). Please consider using the MemoryStream class.");
} }
internal static ICollection<byte[]> GenerateImages(IDocument document) internal static ICollection<byte[]> GenerateImages(IDocument document, ImageGenerationSettings settings)
{ {
ValidateLicense(); ValidateLicense();
var settings = document.GetSettings(); var documentSettings = document.GetSettings();
documentSettings.ImageRasterDpi = settings.RasterDpi;
var canvas = new ImageCanvas(settings); var canvas = new ImageCanvas(settings);
RenderDocument(canvas, document, settings); RenderDocument(canvas, document, documentSettings, useOriginalImages: true);
return canvas.Images; return canvas.Images;
} }
@@ -92,7 +93,7 @@ namespace QuestPDF.Drawing
return canvas.Pictures; return canvas.Pictures;
} }
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings) internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, DocumentSettings settings, bool useOriginalImages = false)
where TCanvas : ICanvas, IRenderingCanvas where TCanvas : ICanvas, IRenderingCanvas
{ {
var container = new DocumentContainer(); var container = new DocumentContainer();
@@ -101,7 +102,7 @@ namespace QuestPDF.Drawing
ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default); ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
ApplyContentDirection(content, settings.ContentDirection); ApplyContentDirection(content, settings.ContentDirection);
ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality); ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null; var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
@@ -228,7 +229,7 @@ namespace QuestPDF.Drawing
ApplyContentDirection(child, direction); ApplyContentDirection(child, direction);
} }
internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality) internal static void ApplyDefaultImageConfiguration(this Element? content, int imageRasterDpi, ImageCompressionQuality imageCompressionQuality, bool useOriginalImages)
{ {
content.VisitChildren(x => content.VisitChildren(x =>
{ {
@@ -236,25 +237,28 @@ namespace QuestPDF.Drawing
{ {
image.TargetDpi ??= imageRasterDpi; image.TargetDpi ??= imageRasterDpi;
image.CompressionQuality ??= imageCompressionQuality; image.CompressionQuality ??= imageCompressionQuality;
image.UseOriginalImage |= useOriginalImages;
} }
if (x is QuestPDF.Elements.DynamicImage dynamicImage) if (x is QuestPDF.Elements.DynamicImage dynamicImage)
{ {
dynamicImage.TargetDpi ??= imageRasterDpi; dynamicImage.TargetDpi ??= imageRasterDpi;
dynamicImage.CompressionQuality ??= imageCompressionQuality; dynamicImage.CompressionQuality ??= imageCompressionQuality;
dynamicImage.UseOriginalImage |= useOriginalImages;
} }
if (x is DynamicHost dynamicHost) if (x is DynamicHost dynamicHost)
{ {
dynamicHost.ImageTargetDpi ??= imageRasterDpi; dynamicHost.ImageTargetDpi ??= imageRasterDpi;
dynamicHost.ImageCompressionQuality ??= imageCompressionQuality; dynamicHost.ImageCompressionQuality ??= imageCompressionQuality;
dynamicHost.UseOriginalImage |= useOriginalImages;
} }
if (x is TextBlock textBlock) if (x is TextBlock textBlock)
{ {
foreach (var textBlockElement in textBlock.Items.OfType<TextBlockElement>()) foreach (var textBlockElement in textBlock.Items.OfType<TextBlockElement>())
{ {
textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality); textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality, useOriginalImages);
} }
} }
}); });
+4 -4
View File
@@ -7,12 +7,12 @@ namespace QuestPDF.Drawing
{ {
internal class ImageCanvas : SkiaCanvasBase internal class ImageCanvas : SkiaCanvasBase
{ {
private DocumentSettings Settings { get; } private ImageGenerationSettings Settings { get; }
private SKSurface Surface { get; set; } private SKSurface Surface { get; set; }
internal ICollection<byte[]> Images { get; } = new List<byte[]>(); internal ICollection<byte[]> Images { get; } = new List<byte[]>();
public ImageCanvas(DocumentSettings settings) public ImageCanvas(ImageGenerationSettings settings)
{ {
Settings = settings; Settings = settings;
} }
@@ -30,7 +30,7 @@ namespace QuestPDF.Drawing
public override void BeginPage(Size size) public override void BeginPage(Size size)
{ {
var scalingFactor = Settings.ImageRasterDpi / (float) PageSizes.PointsPerInch; var scalingFactor = Settings.RasterDpi / (float) PageSizes.PointsPerInch;
var imageInfo = new SKImageInfo((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor)); var imageInfo = new SKImageInfo((int) (size.Width * scalingFactor), (int) (size.Height * scalingFactor));
Surface = SKSurface.Create(imageInfo); Surface = SKSurface.Create(imageInfo);
@@ -42,7 +42,7 @@ namespace QuestPDF.Drawing
public override void EndPage() public override void EndPage()
{ {
Canvas.Save(); Canvas.Save();
var image = Surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray(); var image = Surface.Snapshot().Encode(Settings.ImageFormat.ToSkImageFormat(), Settings.ImageCompressionQuality.ToQualityValue()).ToArray();
Images.Add(image); Images.Add(image);
Canvas.Dispose(); Canvas.Dispose();
+4 -1
View File
@@ -16,6 +16,7 @@ namespace QuestPDF.Elements
internal int? ImageTargetDpi { get; set; } internal int? ImageTargetDpi { get; set; }
internal ImageCompressionQuality? ImageCompressionQuality { get; set; } internal ImageCompressionQuality? ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public DynamicHost(DynamicComponentProxy child) public DynamicHost(DynamicComponentProxy child)
{ {
@@ -63,6 +64,7 @@ namespace QuestPDF.Elements
ImageTargetDpi = ImageTargetDpi.Value, ImageTargetDpi = ImageTargetDpi.Value,
ImageCompressionQuality = ImageCompressionQuality.Value, ImageCompressionQuality = ImageCompressionQuality.Value,
UseOriginalImage = UseOriginalImage,
PageNumber = PageContext.CurrentPage, PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd, TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
@@ -88,6 +90,7 @@ namespace QuestPDF.Elements
internal int ImageTargetDpi { get; set; } internal int ImageTargetDpi { get; set; }
internal ImageCompressionQuality ImageCompressionQuality { get; set; } internal ImageCompressionQuality ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public int PageNumber { get; internal set; } public int PageNumber { get; internal set; }
public int TotalPages { get; internal set; } public int TotalPages { get; internal set; }
@@ -100,7 +103,7 @@ namespace QuestPDF.Elements
container.ApplyInheritedAndGlobalTexStyle(TextStyle); container.ApplyInheritedAndGlobalTexStyle(TextStyle);
container.ApplyContentDirection(ContentDirection); container.ApplyContentDirection(ContentDirection);
container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality); container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage);
container.InjectDependencies(PageContext, Canvas); container.InjectDependencies(PageContext, Canvas);
container.VisitChildren(x => (x as IStateResettable)?.ResetState()); container.VisitChildren(x => (x as IStateResettable)?.ResetState());
+8
View File
@@ -13,6 +13,7 @@ namespace QuestPDF.Elements
{ {
internal int? TargetDpi { get; set; } internal int? TargetDpi { get; set; }
internal ImageCompressionQuality? CompressionQuality { get; set; } internal ImageCompressionQuality? CompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public GenerateDynamicImageDelegate? Source { get; set; } public GenerateDynamicImageDelegate? Source { get; set; }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
@@ -31,6 +32,13 @@ namespace QuestPDF.Elements
return; return;
using var originalImage = SKImage.FromEncodedData(imageData); using var originalImage = SKImage.FromEncodedData(imageData);
if (UseOriginalImage)
{
Canvas.DrawImage(originalImage, Position.Zero, availableSpace);
return;
}
using var compressedImage = originalImage.CompressImage(CompressionQuality.Value); using var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage); var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);
+20 -8
View File
@@ -75,30 +75,42 @@ namespace QuestPDF.Fluent
public static IEnumerable<byte[]> GenerateImages(this IDocument document) public static IEnumerable<byte[]> GenerateImages(this IDocument document)
{ {
return DocumentGenerator.GenerateImages(document); return document.GenerateImages(ImageGenerationSettings.Default);
} }
public static IEnumerable<byte[]> GenerateImages(this IDocument document, ImageGenerationSettings settings)
{
return DocumentGenerator.GenerateImages(document, settings);
}
/// <param name="filePath">Method should return fileName for given index</param> /// <param name="filePath">Method should return fileName for given index</param>
public static void GenerateImages(this IDocument document, Func<int, string> filePath) public static void GenerateImages(this IDocument document, Func<int, string> filePath)
{
document.GenerateImages(filePath, ImageGenerationSettings.Default);
}
/// <param name="filePath">Method should return fileName for given index</param>
public static void GenerateImages(this IDocument document, Func<int, string> filePath, ImageGenerationSettings settings)
{ {
var index = 0; var index = 0;
foreach (var imageData in document.GenerateImages()) foreach (var imageData in document.GenerateImages(settings))
{ {
var path = filePath(index); var path = filePath(index);
if (File.Exists(path)) if (File.Exists(path))
File.Delete(path); File.Delete(path);
File.WriteAllBytes(path, imageData); File.WriteAllBytes(path, imageData);
index++; index++;
} }
} }
#endregion #endregion
#region Helpers #region Helpers
private static void OpenFileUsingDefaultProgram(string filePath) private static void OpenFileUsingDefaultProgram(string filePath)
{ {
var process = new Process var process = new Process
+3 -3
View File
@@ -109,19 +109,19 @@ namespace QuestPDF.Fluent
{ {
public static ImageDescriptor Image(this IContainer parent, byte[] imageData) public static ImageDescriptor Image(this IContainer parent, byte[] imageData)
{ {
var image = Infrastructure.Image.FromBinaryData(imageData).DisposeAfterDocumentGeneration(); var image = Infrastructure.Image.FromBinaryData(imageData);
return parent.Image(image); return parent.Image(image);
} }
public static ImageDescriptor Image(this IContainer parent, string filePath) public static ImageDescriptor Image(this IContainer parent, string filePath)
{ {
var image = Infrastructure.Image.FromFile(filePath).DisposeAfterDocumentGeneration(); var image = Infrastructure.Image.FromFile(filePath);
return parent.Image(image); return parent.Image(image);
} }
public static ImageDescriptor Image(this IContainer parent, Stream fileStream) public static ImageDescriptor Image(this IContainer parent, Stream fileStream)
{ {
var image = Infrastructure.Image.FromStream(fileStream).DisposeAfterDocumentGeneration(); var image = Infrastructure.Image.FromStream(fileStream);
return parent.Image(image); return parent.Image(image);
} }
+11
View File
@@ -62,6 +62,17 @@ namespace QuestPDF.Helpers
return size.Width < 0f || size.Height < 0f; return size.Width < 0f || size.Height < 0f;
} }
internal static SKEncodedImageFormat ToSkImageFormat(this ImageFormat format)
{
return format switch
{
ImageFormat.Jpeg => SKEncodedImageFormat.Jpeg,
ImageFormat.Png => SKEncodedImageFormat.Png,
ImageFormat.Webp=> SKEncodedImageFormat.Webp,
_ => throw new ArgumentOutOfRangeException(nameof(format), format, null)
};
}
internal static int ToQualityValue(this ImageCompressionQuality quality) internal static int ToQualityValue(this ImageCompressionQuality quality)
{ {
return quality switch return quality switch
+47 -10
View File
@@ -13,28 +13,29 @@ namespace QuestPDF.Infrastructure
internal ImageCompressionQuality CompressionQuality { get; set; } internal ImageCompressionQuality CompressionQuality { get; set; }
} }
public class Image : IDisposable public class Image
{ {
internal SKImage SkImage { get; } internal SKImage SkImage { get; }
internal ImageSize Size { get; } internal ImageSize Size { get; }
internal bool IsDocumentScoped { get; set; } internal bool IsDocumentScoped { get; }
internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new(); internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
private Image(SKImage image) internal Image(SKImage image, bool isDocumentScoped)
{ {
SkImage = image; SkImage = image;
IsDocumentScoped = isDocumentScoped;
Size = new ImageSize(image.Width, image.Height); Size = new ImageSize(image.Width, image.Height);
} }
public void Dispose() internal void Dispose()
{ {
SkImage.Dispose(); SkImage.Dispose();
foreach (var cacheKey in ScaledImageCache) foreach (var cacheKey in ScaledImageCache)
cacheKey.image.Dispose(); cacheKey.image.Dispose();
} }
#region Scaling Image #region Scaling Image
internal SKImage GetVersionOfSize(GetImageVersionRequest request) internal SKImage GetVersionOfSize(GetImageVersionRequest request)
@@ -79,15 +80,51 @@ namespace QuestPDF.Infrastructure
if (image == null) if (image == null)
throw new DocumentComposeException("Cannot load or decode provided image."); throw new DocumentComposeException("Cannot load or decode provided image.");
return new Image(image); return new Image(image, true);
} }
#endregion #endregion
}
internal Image DisposeAfterDocumentGeneration() public class SharedImage : Image, IDisposable
{
internal SharedImage(SKImage image) : base(image, false)
{ {
IsDocumentScoped = true;
return this;
} }
public void Dispose()
{
base.Dispose();
}
#region public constructors
public static new SharedImage FromBinaryData(byte[] imageData)
{
return CreateImage(SKImage.FromEncodedData(imageData));
}
public static new SharedImage FromFile(string filePath)
{
return CreateImage(SKImage.FromEncodedData(filePath));
}
public static new SharedImage FromStream(Stream fileStream)
{
return CreateImage(SKImage.FromEncodedData(fileStream));
}
private static SharedImage CreateImage(SKImage? image)
{
if (image == null)
throw new DocumentComposeException("Cannot load or decode provided image.");
var result = new SharedImage(image);
return result;
}
#endregion
} }
} }
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
public enum ImageFormat
{
Jpeg,
Png,
Webp
}
}
@@ -0,0 +1,27 @@
using SkiaSharp;
namespace QuestPDF.Infrastructure
{
public class ImageGenerationSettings
{
/// <summary>
/// The file format used to encode the image(s).
/// </summary>
public ImageFormat ImageFormat { get; set; } = ImageFormat.Png;
/// <summary>
/// Encoding quality controls the trade-off between size and quality.
/// The default value is "high quality".
/// </summary>
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
/// <summary>
/// The DPI (pixels-per-inch) at which the document will be rasterized. This parameter controls the resolution of produced images.
/// Default value is 144.
/// </summary>
public int RasterDpi { get; set; } = DocumentSettings.DefaultRasterDpi * 2;
public static ImageGenerationSettings Default => new ImageGenerationSettings();
}
}