Compare commits

...

4 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
13 changed files with 135 additions and 33 deletions
+34 -5
View File
@@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
@@ -79,11 +80,11 @@ namespace QuestPDF.Examples
.ShowResults()
.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);
});
});
@@ -99,9 +100,9 @@ namespace QuestPDF.Examples
.ShowResults()
.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.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]
public void Exception()
{
@@ -38,6 +38,9 @@
<None Update="photo.jpg">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="checkbox.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</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 =>
{
var sharedImage = DocumentImage.FromBinaryData(photo).DisposeAfterDocumentGeneration();
var sharedImage = DocumentImage.FromBinaryData(photo);
foreach (var i in Enumerable.Range(0, 10))
column.Item().Image(sharedImage);
+8 -5
View File
@@ -52,7 +52,7 @@ namespace QuestPDF.Drawing
var documentSettings = document.GetSettings();
documentSettings.ImageRasterDpi = settings.RasterDpi;
var canvas = new ImageCanvas(settings);
RenderDocument(canvas, document, documentSettings);
RenderDocument(canvas, document, documentSettings, useOriginalImages: true);
return canvas.Images;
}
@@ -93,7 +93,7 @@ namespace QuestPDF.Drawing
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
{
var container = new DocumentContainer();
@@ -102,7 +102,7 @@ namespace QuestPDF.Drawing
ApplyInheritedAndGlobalTexStyle(content, TextStyle.Default);
ApplyContentDirection(content, settings.ContentDirection);
ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality);
ApplyDefaultImageConfiguration(content, settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
@@ -229,7 +229,7 @@ namespace QuestPDF.Drawing
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 =>
{
@@ -237,25 +237,28 @@ namespace QuestPDF.Drawing
{
image.TargetDpi ??= imageRasterDpi;
image.CompressionQuality ??= imageCompressionQuality;
image.UseOriginalImage |= useOriginalImages;
}
if (x is QuestPDF.Elements.DynamicImage dynamicImage)
{
dynamicImage.TargetDpi ??= imageRasterDpi;
dynamicImage.CompressionQuality ??= imageCompressionQuality;
dynamicImage.UseOriginalImage |= useOriginalImages;
}
if (x is DynamicHost dynamicHost)
{
dynamicHost.ImageTargetDpi ??= imageRasterDpi;
dynamicHost.ImageCompressionQuality ??= imageCompressionQuality;
dynamicHost.UseOriginalImage |= useOriginalImages;
}
if (x is TextBlock textBlock)
{
foreach (var textBlockElement in textBlock.Items.OfType<TextBlockElement>())
{
textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality);
textBlockElement.Element.ApplyDefaultImageConfiguration(imageRasterDpi, imageCompressionQuality, useOriginalImages);
}
}
});
+1 -1
View File
@@ -42,7 +42,7 @@ namespace QuestPDF.Drawing
public override void EndPage()
{
Canvas.Save();
var image = Surface.Snapshot().Encode(Settings.Format, Settings.Quality).ToArray();
var image = Surface.Snapshot().Encode(Settings.ImageFormat.ToSkImageFormat(), Settings.ImageCompressionQuality.ToQualityValue()).ToArray();
Images.Add(image);
Canvas.Dispose();
+4 -1
View File
@@ -16,6 +16,7 @@ namespace QuestPDF.Elements
internal int? ImageTargetDpi { get; set; }
internal ImageCompressionQuality? ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public DynamicHost(DynamicComponentProxy child)
{
@@ -63,6 +64,7 @@ namespace QuestPDF.Elements
ImageTargetDpi = ImageTargetDpi.Value,
ImageCompressionQuality = ImageCompressionQuality.Value,
UseOriginalImage = UseOriginalImage,
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
@@ -88,6 +90,7 @@ namespace QuestPDF.Elements
internal int ImageTargetDpi { get; set; }
internal ImageCompressionQuality ImageCompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public int PageNumber { get; internal set; }
public int TotalPages { get; internal set; }
@@ -100,7 +103,7 @@ namespace QuestPDF.Elements
container.ApplyInheritedAndGlobalTexStyle(TextStyle);
container.ApplyContentDirection(ContentDirection);
container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality);
container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage);
container.InjectDependencies(PageContext, Canvas);
container.VisitChildren(x => (x as IStateResettable)?.ResetState());
+8
View File
@@ -13,6 +13,7 @@ namespace QuestPDF.Elements
{
internal int? TargetDpi { get; set; }
internal ImageCompressionQuality? CompressionQuality { get; set; }
internal bool UseOriginalImage { get; set; }
public GenerateDynamicImageDelegate? Source { get; set; }
internal override SpacePlan Measure(Size availableSpace)
@@ -31,6 +32,13 @@ namespace QuestPDF.Elements
return;
using var originalImage = SKImage.FromEncodedData(imageData);
if (UseOriginalImage)
{
Canvas.DrawImage(originalImage, Position.Zero, availableSpace);
return;
}
using var compressedImage = originalImage.CompressImage(CompressionQuality.Value);
var targetImage = Helpers.Helpers.GetImageWithSmallerSize(originalImage, compressedImage);
+3 -3
View File
@@ -109,19 +109,19 @@ namespace QuestPDF.Fluent
{
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);
}
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);
}
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);
}
+11
View File
@@ -62,6 +62,17 @@ namespace QuestPDF.Helpers
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)
{
return quality switch
+47 -10
View File
@@ -13,28 +13,29 @@ namespace QuestPDF.Infrastructure
internal ImageCompressionQuality CompressionQuality { get; set; }
}
public class Image : IDisposable
public class Image
{
internal SKImage SkImage { get; }
internal ImageSize Size { get; }
internal bool IsDocumentScoped { get; set; }
internal bool IsDocumentScoped { get; }
internal LinkedList<(GetImageVersionRequest request, SKImage image)> ScaledImageCache { get; } = new();
private Image(SKImage image)
internal Image(SKImage image, bool isDocumentScoped)
{
SkImage = image;
IsDocumentScoped = isDocumentScoped;
Size = new ImageSize(image.Width, image.Height);
}
public void Dispose()
internal void Dispose()
{
SkImage.Dispose();
foreach (var cacheKey in ScaledImageCache)
cacheKey.image.Dispose();
}
#region Scaling Image
internal SKImage GetVersionOfSize(GetImageVersionRequest request)
@@ -79,15 +80,51 @@ namespace QuestPDF.Infrastructure
if (image == null)
throw new DocumentComposeException("Cannot load or decode provided image.");
return new Image(image);
return new Image(image, true);
}
#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
}
}
@@ -7,20 +7,19 @@ namespace QuestPDF.Infrastructure
/// <summary>
/// The file format used to encode the image(s).
/// </summary>
public SKEncodedImageFormat Format { get; set; } = SKEncodedImageFormat.Png;
public ImageFormat ImageFormat { get; set; } = ImageFormat.Png;
/// <summary>
/// The quality level to use for the image(s). This is in the range from 0-100.
/// Encoding quality controls the trade-off between size and quality.
/// The default value is "high quality".
/// </summary>
public int Quality { get; set; } = 100;
public ImageCompressionQuality ImageCompressionQuality { get; set; } = ImageCompressionQuality.VeryHigh;
/// <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.
/// 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; } = 144;
public int RasterDpi { get; set; } = DocumentSettings.DefaultRasterDpi * 2;
public static ImageGenerationSettings Default => new ImageGenerationSettings();