From 2a55e7b650549705a81cd0039ed94234664d81a4 Mon Sep 17 00:00:00 2001 From: X39 Date: Wed, 20 Dec 2023 22:00:28 +0100 Subject: [PATCH] image control and reworking ColumnLength into adding onto Length [major] Kinda F-ed up the commits in here because i was lazy ... anyways, plenty of fancy things happening in this, mostly untested because i did not yet bother to write the tests. Gotta go to sleep now tho and want to commit this to get a clean state again. --- X39.Solutions.PdfTemplate.sln | 8 +- .../Abstraction/CanvasImpl.cs | 8 + .../Abstraction/ICanvas.cs | 7 + .../Controls/Base/AlignableContentControl.cs | 31 ++- .../Controls/ImageControl.cs | 128 +++++++++++ .../Controls/TableControl.cs | 50 ++--- .../Data/ColumnLength.cs | 176 +++++---------- .../Data/EColumnUnit.cs | 16 +- .../Data/{ELengthMode.cs => ELengthUnit.cs} | 26 ++- .../X39.Solutions.PdfTemplate/Data/Length.cs | 202 +++++++++++------- .../Data/Thickness.cs | 45 ++-- source/X39.Solutions.PdfTemplate/Generator.cs | 5 +- .../IInitializeAsync.cs | 14 ++ .../ServiceCollectionExtensions.cs | 17 ++ .../DefaultResourceResolver.cs | 31 +++ .../ResourceResolver/IResourceResolver.cs | 21 ++ source/X39.Solutions.PdfTemplate/Template.cs | 106 ++++++++- .../Controls/LineControlTests.cs | 52 ++--- .../Controls/TableControlTest.cs | 10 +- .../Mock/CanvasMock.cs | 3 + .../Parsing/ThicknessParsingTests.cs | 48 ++--- test/images/X.jpg | Bin 0 -> 166477 bytes 22 files changed, 681 insertions(+), 323 deletions(-) create mode 100644 source/X39.Solutions.PdfTemplate/Controls/ImageControl.cs rename source/X39.Solutions.PdfTemplate/Data/{ELengthMode.cs => ELengthUnit.cs} (54%) create mode 100644 source/X39.Solutions.PdfTemplate/IInitializeAsync.cs create mode 100644 source/X39.Solutions.PdfTemplate/Services/ResourceResolver/DefaultResourceResolver.cs create mode 100644 source/X39.Solutions.PdfTemplate/Services/ResourceResolver/IResourceResolver.cs create mode 100644 test/images/X.jpg diff --git a/X39.Solutions.PdfTemplate.sln b/X39.Solutions.PdfTemplate.sln index 9c2221c..6903220 100644 --- a/X39.Solutions.PdfTemplate.sln +++ b/X39.Solutions.PdfTemplate.sln @@ -28,6 +28,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fonts", "fonts", "{AC1F6F58 test\fonts\Nunito_Sans\NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf = test\fonts\Nunito_Sans\NunitoSans-VariableFont_YTLC,opsz,wdth,wght.ttf EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "images", "images", "{41BE3CC0-0C65-4D1A-AD6C-90B01D132C45}" + ProjectSection(SolutionItems) = preProject + test\images\X.jpg = test\images\X.jpg + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -47,6 +52,7 @@ Global {C62AB41B-AC9D-4946-84D7-A22A63B28CF0} = {E290E22A-D697-4B30-A067-AF972686C3B5} {54D29B97-100B-4494-BA96-DF9CBA107F8B} = {578183F3-B424-4D0B-9B6B-1F22FBDB80C8} {B7C9C376-E1AA-46D0-90C6-D6BEAEB658EC} = {A16F2F3F-BF45-4A18-8E4A-31B800226E47} - {AC1F6F58-BADB-4BA3-80E6-A4F938455800} = {A16F2F3F-BF45-4A18-8E4A-31B800226E47} + {AC1F6F58-BADB-4BA3-80E6-A4F938455800} = {578183F3-B424-4D0B-9B6B-1F22FBDB80C8} + {41BE3CC0-0C65-4D1A-AD6C-90B01D132C45} = {578183F3-B424-4D0B-9B6B-1F22FBDB80C8} EndGlobalSection EndGlobal diff --git a/source/X39.Solutions.PdfTemplate/Abstraction/CanvasImpl.cs b/source/X39.Solutions.PdfTemplate/Abstraction/CanvasImpl.cs index 9bc4209..b4cdc85 100644 --- a/source/X39.Solutions.PdfTemplate/Abstraction/CanvasImpl.cs +++ b/source/X39.Solutions.PdfTemplate/Abstraction/CanvasImpl.cs @@ -80,4 +80,12 @@ public void DrawBitmap(byte[] bytes, Rectangle rectangle) canvas.DrawBitmap(bitmap, rectangle); }); } + + public void DrawBitmap(SKBitmap bitmap, Rectangle rectangle) + { + _drawActions.Add((canvas) => + { + canvas.DrawBitmap(bitmap, rectangle); + }); + } } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Abstraction/ICanvas.cs b/source/X39.Solutions.PdfTemplate/Abstraction/ICanvas.cs index d9b82c1..0c1d44d 100644 --- a/source/X39.Solutions.PdfTemplate/Abstraction/ICanvas.cs +++ b/source/X39.Solutions.PdfTemplate/Abstraction/ICanvas.cs @@ -73,4 +73,11 @@ public interface ICanvas /// The bitmap to draw. /// The region to draw the bitmap into. void DrawBitmap(byte[] bitmap, Rectangle rectangle); + + /// + /// Draws a bitmap on the canvas. + /// + /// The SkiaSharp bitmap to draw. + /// The region to draw the bitmap into. + void DrawBitmap(SKBitmap bitmap, Rectangle arrangementInner); } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Controls/Base/AlignableContentControl.cs b/source/X39.Solutions.PdfTemplate/Controls/Base/AlignableContentControl.cs index 3b2610c..92a7505 100644 --- a/source/X39.Solutions.PdfTemplate/Controls/Base/AlignableContentControl.cs +++ b/source/X39.Solutions.PdfTemplate/Controls/Base/AlignableContentControl.cs @@ -5,10 +5,11 @@ namespace X39.Solutions.PdfTemplate.Controls.Base; /// /// Base class for alignable controls that can contain other controls. /// -public abstract class AlignableContentControl : AlignableControl, IContentControl +[PublicAPI] +public abstract class AlignableContentControl : AlignableControl, IContentControl, IAsyncDisposable { private readonly List _children = new(); - + /// /// The children of this content control. /// @@ -43,4 +44,30 @@ public abstract class AlignableContentControl : AlignableControl, IContentContro /// public abstract bool CanAdd(Type type); + + /// + /// Dispose pattern method, that can be overridden by derived classes. + /// + /// + /// Always call the base implementation! + /// + protected virtual async ValueTask DisposeAsyncCore() + { + foreach (var child in _children) + { + // ReSharper disable once SuspiciousTypeConversion.Global + if (child is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + // ReSharper disable once SuspiciousTypeConversion.Global + else if (child is IDisposable disposable) + disposable.Dispose(); + } + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + GC.SuppressFinalize(this); + } } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Controls/ImageControl.cs b/source/X39.Solutions.PdfTemplate/Controls/ImageControl.cs new file mode 100644 index 0000000..f14717a --- /dev/null +++ b/source/X39.Solutions.PdfTemplate/Controls/ImageControl.cs @@ -0,0 +1,128 @@ +using System.Globalization; +using SkiaSharp; +using X39.Solutions.PdfTemplate.Abstraction; +using X39.Solutions.PdfTemplate.Attributes; +using X39.Solutions.PdfTemplate.Controls.Base; +using X39.Solutions.PdfTemplate.Data; +using X39.Solutions.PdfTemplate.Services.ResourceResolver; + +namespace X39.Solutions.PdfTemplate.Controls; + +/// +/// A control that draws an image. +/// +[Control(Constants.ControlsNamespace)] +public sealed class ImageControl : AlignableControl, IInitializeAsync, IDisposable +{ + private readonly IResourceResolver _resourceResolver; + + /// + /// Creates a new . + /// + /// The to use. + public ImageControl(IResourceResolver resourceResolver) + { + _resourceResolver = resourceResolver; + } + + /// + /// The source of the image to draw. + /// + /// + /// This always has to be resolved, using a . + /// The default implementation of is . + /// It will only accept base64 encoded images for security reasons! + /// Make sure to provide your own if you + /// want to use other sources, like a file path. + /// + [Parameter] + public string Source { get; set; } = string.Empty; + + /// + /// The width of the image. + /// + [Parameter] + public Length Width { get; set; } = new(); + + /// + /// The height of the image. + /// + [Parameter] + public Length Height { get; set; } = new(); + + private SKBitmap? _bitmap; + + /// + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + var image = await _resourceResolver.ResolveImageAsync(Source, cancellationToken) + .ConfigureAwait(false); + _bitmap = SKBitmap.Decode(image); + } + + /// + public void Dispose() + { + _bitmap?.Dispose(); + } + + /// + protected override Size DoMeasure( + float dpi, + in Size fullPageSize, + in Size framedPageSize, + in Size remainingSize, + CultureInfo cultureInfo) + { + var width = Width.ToPixels(framedPageSize.Width, dpi); + var bitmapWidth = _bitmap?.Width ?? 0F; + var height = Height.ToPixels(framedPageSize.Height, dpi); + var bitmapHeight = _bitmap?.Height ?? 0F; + return new Size( + Width.Unit is ELengthUnit.Auto + ? Height.Unit is ELengthUnit.Auto + ? bitmapWidth + : bitmapWidth / bitmapHeight * height + : width, + Height.Unit is ELengthUnit.Auto + ? Width.Unit is ELengthUnit.Auto + ? bitmapHeight + : bitmapHeight / bitmapWidth * width + : height + ); + } + + /// + protected override Size DoArrange( + float dpi, + in Size fullPageSize, + in Size framedPageSize, + in Size remainingSize, + CultureInfo cultureInfo) + { + var width = Width.ToPixels(framedPageSize.Width, dpi); + var bitmapWidth = _bitmap?.Width ?? 0F; + var height = Height.ToPixels(framedPageSize.Height, dpi); + var bitmapHeight = _bitmap?.Height ?? 0F; + return new Size( + Width.Unit is ELengthUnit.Auto + ? Height.Unit is ELengthUnit.Auto + ? bitmapWidth + : bitmapWidth / bitmapHeight * height + : width, + Height.Unit is ELengthUnit.Auto + ? Width.Unit is ELengthUnit.Auto + ? bitmapHeight + : bitmapHeight / bitmapWidth * width + : height + ); + } + + /// + protected override void DoRender(ICanvas canvas, float dpi, in Size parentSize, CultureInfo cultureInfo) + { + if (_bitmap is null) + return; + canvas.DrawBitmap(_bitmap, ArrangementInner - Arrangement); + } +} \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Controls/TableControl.cs b/source/X39.Solutions.PdfTemplate/Controls/TableControl.cs index 59a1dd7..71cf3b6 100644 --- a/source/X39.Solutions.PdfTemplate/Controls/TableControl.cs +++ b/source/X39.Solutions.PdfTemplate/Controls/TableControl.cs @@ -83,6 +83,7 @@ HorizontalAlignment is EHorizontalAlignment.Stretch var outWidths = CalculateWidths( desiredTotalWidth, + dpi, CellWidths.Values .Select((q) => (q.columnLength, q.desiredWitdth)) .ToArray()); @@ -99,30 +100,26 @@ HorizontalAlignment is EHorizontalAlignment.Stretch private static IReadOnlyCollection CalculateWidths( float totalWidth, + float dpi, IReadOnlyCollection<(ColumnLength length, float desiredWidth)> columns) { - var totalPixelWidth = columns - .Where((q) => q.length.Unit == EColumnUnit.Pixel) - .Select((q) => q.length.Value) - .DefaultIfEmpty() - .Sum(); - var totalPercentWidth = columns - .Where((q) => q.length.Unit == EColumnUnit.Percent) - .Select((q) => q.length.Value * totalWidth) + var totalFixedWidth = columns + .Where((q) => q.length.Unit == EColumnUnit.Lenght && q.length.Length?.Unit != ELengthUnit.Auto) + .Select((q) => q.length.Length?.ToPixels(totalWidth, dpi)) + .NotNull() .DefaultIfEmpty() .Sum(); var remainingWidth = totalWidth; - remainingWidth -= totalPixelWidth; - remainingWidth -= totalPercentWidth; + remainingWidth -= totalFixedWidth; var totalParts = columns - .Where((q) => q.length.Unit == EColumnUnit.Part) + .Where((q) => q.length.Unit == EColumnUnit.Parts) .Select((q) => q.length.Value) .DefaultIfEmpty() .Sum(); var totalAutoWidth = columns - .Where((q) => q.length.Unit == EColumnUnit.Auto) + .Where((q) => q.length is {Unit: EColumnUnit.Lenght, Length.Unit: ELengthUnit.Auto}) .Select((q) => q.desiredWidth) .Sum(); @@ -138,10 +135,12 @@ private static IReadOnlyCollection CalculateWidths( { outWidths[index] = length.Unit switch { - EColumnUnit.Auto => desiredWidth * autoCoef, - EColumnUnit.Pixel => length.Value, - EColumnUnit.Part => partWidth * length.Value, - EColumnUnit.Percent => totalWidth * length.Value, + EColumnUnit.Parts => partWidth * length.Value ?? 0F, + EColumnUnit.Lenght => length.Length?.Unit switch + { + ELengthUnit.Auto => desiredWidth * autoCoef, + _ => length.Length?.ToPixels(totalWidth, dpi) ?? 0F, + }, _ => throw new InvalidEnumArgumentException( nameof(length.Unit), (int) length.Unit, @@ -155,13 +154,13 @@ private static IReadOnlyCollection CalculateWidths( { var max = totalWidth / columns.Count; var larger = columns - .Where(x => x.length.Unit == EColumnUnit.Auto) + .Where(x => x.length is {Unit: EColumnUnit.Lenght, Length.Unit: ELengthUnit.Auto}) .Where(x => x.desiredWidth > max) .DefaultIfEmpty() .Sum(x => x.desiredWidth); var remainingWidthWithoutLarger = remainingWidth - larger; var remainingCount = columns - .Where(x => x.length.Unit == EColumnUnit.Auto) + .Where(x => x.length is {Unit: EColumnUnit.Lenght, Length.Unit: ELengthUnit.Auto}) .Count(x => x.desiredWidth <= max); var newWidth = remainingWidthWithoutLarger / remainingCount; var outWidths = new float[columns.Count]; @@ -169,12 +168,14 @@ private static IReadOnlyCollection CalculateWidths( { outWidths[index] = length.Unit switch { - EColumnUnit.Auto => desiredWidth > max || autoCoef < 1.0F - ? desiredWidth * autoCoef - : newWidth, - EColumnUnit.Pixel => length.Value, - EColumnUnit.Part => 0F, // We don't have any parts in this branch - EColumnUnit.Percent => totalWidth * length.Value, + EColumnUnit.Parts => 0F, // We don't have any parts in this branch + EColumnUnit.Lenght => length.Length?.Unit switch + { + ELengthUnit.Auto => desiredWidth > max || autoCoef < 1.0F + ? desiredWidth * autoCoef + : newWidth, + _ => length.Length?.ToPixels(totalWidth, dpi) ?? 0F, + }, _ => throw new InvalidEnumArgumentException( nameof(length.Unit), (int) length.Unit, @@ -196,6 +197,7 @@ protected override Size DoArrange( { var outWidths = CalculateWidths( framedPageSize.Width, + dpi, CellWidths.Values .Select((q) => (q.columnLength, q.desiredWitdth)) .ToArray()); diff --git a/source/X39.Solutions.PdfTemplate/Data/ColumnLength.cs b/source/X39.Solutions.PdfTemplate/Data/ColumnLength.cs index 2b36993..9dc2183 100644 --- a/source/X39.Solutions.PdfTemplate/Data/ColumnLength.cs +++ b/source/X39.Solutions.PdfTemplate/Data/ColumnLength.cs @@ -13,19 +13,28 @@ namespace X39.Solutions.PdfTemplate.Data; /// /// Creates a new which will fit the available space. /// - public ColumnLength() : this(1.0F, EColumnUnit.Auto) + public ColumnLength() : this(new Length(default, ELengthUnit.Auto)) { } /// - /// Creates a new with the given value and + /// Creates a new with the given parts value. /// /// The value of the size - /// The size mode, indicating how the value should be interpreted - public ColumnLength(float value, EColumnUnit unit) + public ColumnLength(float value) { Value = value; - Unit = unit; + Unit = EColumnUnit.Parts; + } + + /// + /// Creates a new with the given length + /// + /// The value of the size + public ColumnLength(Length length) + { + Length = length; + Unit = EColumnUnit.Lenght; } /// @@ -36,7 +45,18 @@ public ColumnLength(float value, EColumnUnit unit) /// /// The value of the size. /// - public float Value { get; init; } + /// + /// Either this or must be set. + /// + public float? Value { get; init; } + + /// + /// The value of the size. + /// + /// + /// Either this or must be set. + /// + public Length? Length { get; init; } /// public static ColumnLength Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider); @@ -56,7 +76,6 @@ public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out return false; } - EColumnUnit lengthMode; var endOfNumber = 0; var dotFound = false; // Find end of number @@ -90,123 +109,33 @@ public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out switch (unit) { - case "%": - lengthMode = EColumnUnit.Percent; - break; - case "px": - case "Px": - case "pX": - case "PX": - lengthMode = EColumnUnit.Pixel; - break; - case "auto": - case "autO": - case "auTo": - case "auTO": - case "aUto": - case "aUtO": - case "aUTo": - case "aUTO": - case "Auto": - case "AutO": - case "AuTo": - case "AuTO": - case "AUto": - case "AUtO": - case "AUTo": - case "AUTO": - lengthMode = EColumnUnit.Auto; - break; - default: - result = default; - return false; - } + case "*": + if (number.IsEmpty) + { + result = default; + return false; + } - if (number.IsEmpty && lengthMode is not EColumnUnit.Auto and EColumnUnit.Part) - { - result = default; - return false; + var value = float.Parse(number, CultureInfo.InvariantCulture); + result = new ColumnLength(value); + return true; + default: + if (!Data.Length.TryParse(s, provider, out var length)) + { + result = default; + return false; + } + result = new ColumnLength(length); + return true; } - - var value = number.IsEmpty ? 1F : float.Parse(number, provider); - result = new ColumnLength(value, lengthMode); - return true; } /// public static ColumnLength Parse(ReadOnlySpan s, IFormatProvider? provider) { - s = s.Trim(); - if (s.IsEmpty) - throw new ArgumentException("The string must not be empty.", nameof(s)); - - EColumnUnit lengthMode; - var endOfNumber = 0; - var dotFound = false; - // Find end of number - for (var i = 0; i < s.Length; i++) - { - if (s[i].IsDigit()) - continue; - if (s[i] == '.') - { - if (dotFound) - throw new FormatException("The string contains more than one dot."); - dotFound = true; - continue; - } - - endOfNumber = i; - break; - } - - var number = s[..endOfNumber]; - var unit = s[endOfNumber..].Trim(); - if (unit.IsEmpty) - throw new FormatException("The string does not contain a unit."); - - switch (unit) - { - case "%": - lengthMode = EColumnUnit.Percent; - break; - case "*": - lengthMode = EColumnUnit.Part; - break; - case "px": - case "Px": - case "pX": - case "PX": - lengthMode = EColumnUnit.Pixel; - break; - case "auto": - case "autO": - case "auTo": - case "auTO": - case "aUto": - case "aUtO": - case "aUTo": - case "aUTO": - case "Auto": - case "AutO": - case "AuTo": - case "AuTO": - case "AUto": - case "AUtO": - case "AUTo": - case "AUTO": - lengthMode = EColumnUnit.Auto; - break; - default: - throw new NotSupportedException($"The unit '{unit}' is not supported."); - } - - if (number.IsEmpty && lengthMode is not EColumnUnit.Auto && lengthMode is not EColumnUnit.Part) - throw new FormatException("The string does not contain a number."); - var value = number.IsEmpty ? 1F : float.Parse(number, provider); - if (lengthMode is EColumnUnit.Percent) - value /= 100.0F; - return new ColumnLength(value, lengthMode); + if (!TryParse(s, provider, out var result)) + throw new FormatException($"The given string '{s.ToString()}' is not a valid {nameof(ColumnLength)}"); + return result; } /// @@ -222,16 +151,11 @@ public static ColumnLength Parse(ReadOnlySpan s, IFormatProvider? provider /// public string ToString(IFormatProvider? provider) { - var unit = Unit switch + return Unit switch { - EColumnUnit.Auto => "auto", - EColumnUnit.Part => "*", - EColumnUnit.Percent => "%", - EColumnUnit.Pixel => "px", - _ => throw new InvalidEnumArgumentException(nameof(Unit), (int) Unit, typeof(EColumnUnit)), + EColumnUnit.Parts => string.Format(provider, "{0}*", Value), + EColumnUnit.Lenght => Length?.ToString(provider) ?? throw new InvalidOperationException("Length is null"), + _ => throw new InvalidEnumArgumentException(nameof(Unit), (int) Unit, typeof(EColumnUnit)), }; - return Unit is EColumnUnit.Auto - ? unit - : $"{Value.ToString(provider)}{unit}"; } } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Data/EColumnUnit.cs b/source/X39.Solutions.PdfTemplate/Data/EColumnUnit.cs index 97541b7..f7e28a8 100644 --- a/source/X39.Solutions.PdfTemplate/Data/EColumnUnit.cs +++ b/source/X39.Solutions.PdfTemplate/Data/EColumnUnit.cs @@ -5,23 +5,13 @@ /// public enum EColumnUnit { - /// - /// The size is automatically calculated. - /// - Auto, - - /// - /// The size is in pixels. - /// - Pixel, - /// /// The size is in parts of the available space. /// - Part, + Parts, /// - /// The size is in percent of the available space. + /// The size is specified in . /// - Percent, + Lenght, } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Data/ELengthMode.cs b/source/X39.Solutions.PdfTemplate/Data/ELengthUnit.cs similarity index 54% rename from source/X39.Solutions.PdfTemplate/Data/ELengthMode.cs rename to source/X39.Solutions.PdfTemplate/Data/ELengthUnit.cs index ae4ecc6..ec0e997 100644 --- a/source/X39.Solutions.PdfTemplate/Data/ELengthMode.cs +++ b/source/X39.Solutions.PdfTemplate/Data/ELengthUnit.cs @@ -3,18 +3,23 @@ namespace X39.Solutions.PdfTemplate.Data; /// /// Enum for the size mode of a /// -public enum ELengthMode +public enum ELengthUnit { + /// + /// The size is automatically determined. + /// + Auto, + /// /// The size is in pixels. /// Pixel, - + /// /// The size is in percent of the available space. /// Percent, - + /// /// The size is in points. /// @@ -22,4 +27,19 @@ public enum ELengthMode /// 1 point = 1/72.272 inch /// Points, + + /// + /// The size is in millimeters. + /// + Millimeters, + + /// + /// The size is in centimeters. + /// + Centimeters, + + /// + /// The size is in inches. + /// + Inches, } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Data/Length.cs b/source/X39.Solutions.PdfTemplate/Data/Length.cs index 64df6ff..246b2af 100644 --- a/source/X39.Solutions.PdfTemplate/Data/Length.cs +++ b/source/X39.Solutions.PdfTemplate/Data/Length.cs @@ -4,7 +4,7 @@ namespace X39.Solutions.PdfTemplate.Data; /// -/// Defines a size with a value and a +/// Defines a size with a value and a /// [TypeConverter(typeof(LengthConverter))] [PublicAPI] @@ -13,60 +13,64 @@ namespace X39.Solutions.PdfTemplate.Data; /// /// Creates a new which will fit the available space. /// - public Length() : this(1.0F, ELengthMode.Percent) + public Length() : this(default, ELengthUnit.Auto) { } /// - /// Defines a size with a value and a + /// Defines a size with a value and a /// /// The value of the size - /// The size mode, indicating how the value should be interpreted - public Length(float value, ELengthMode lengthMode) + /// The size mode, indicating how the value should be interpreted + public Length(float value, ELengthUnit unit) { - Value = value; - LengthMode = lengthMode; + Value = value; + Unit = unit; } /// The value of the size public float Value { get; init; } - /// The size mode, indicating how the value should be interpreted - public ELengthMode LengthMode { get; init; } + /// The size unit, indicating how the value should be interpreted + public ELengthUnit Unit { get; init; } /// - /// Deconstructs the into a and a + /// Deconstructs the into a and a /// /// The value of the size - /// The mode of the size - public void Deconstruct(out float value, out ELengthMode lengthMode) + /// The unit of the size + public void Deconstruct(out float value, out ELengthUnit lengthUnit) { - value = Value; - lengthMode = LengthMode; + value = Value; + lengthUnit = Unit; } - + /// - /// Implicitly converts a into a with + /// Implicitly converts a into a with /// - /// The value of the size - /// A new with - public static implicit operator Length(float value) => new(value, ELengthMode.Pixel); + /// The value of the size + /// A new with + public static implicit operator Length(float valuePx) => new(valuePx, ELengthUnit.Pixel); /// - /// Translates the into a based on the given bounds and . + /// Translates the into a based on the given bounds and . /// - /// The bounds to use for the calculation in case of in pixels - /// The DPI to use for the calculation in case of in pixels + /// The bounds to use for the calculation in case of in pixels + /// The DPI to use for the calculation in case of in pixels /// The pixel value of the - /// Thrown when is not a valid - public float ToPixels(float bounds, float dpi) + /// Thrown when is not a valid + public float ToPixels(float boundsPx, float dpi) { - return LengthMode switch + return Unit switch { - ELengthMode.Pixel => Value, - ELengthMode.Percent => Value * bounds, - ELengthMode.Points => Value * dpi / 72.272F, - _ => throw new InvalidEnumArgumentException(nameof(LengthMode), (int)LengthMode, typeof(ELengthMode)), + ELengthUnit.Pixel => Value, + ELengthUnit.Percent => Value * boundsPx, + ELengthUnit.Points => Value * dpi / 72.272F, + ELengthUnit.Auto => boundsPx, + ELengthUnit.Millimeters => Value * dpi * 0.0393701F, + ELengthUnit.Centimeters => Value * dpi * 0.393701F, + ELengthUnit.Inches => Value * dpi, + _ => throw new InvalidEnumArgumentException(nameof(Unit), (int) Unit, typeof(ELengthUnit)), }; } @@ -74,80 +78,126 @@ public float ToPixels(float bounds, float dpi) public static Length Parse(string s, IFormatProvider? provider) => Parse(s.AsSpan(), provider); /// - public static bool TryParse(string? s, IFormatProvider? provider, out Length result) => TryParse(s.AsSpan(), provider, out result); + public static bool TryParse(string? s, IFormatProvider? provider, out Length result) => + TryParse(s.AsSpan(), provider, out result); /// public static Length Parse(ReadOnlySpan s, IFormatProvider? provider) { - var unitLength = 0; - for (var i = s.Length - 1; i >= 0 && !s[i].IsDigit() && s[i] is not '.'; i--) - unitLength++; - var unit = s[^unitLength..]; - var number = s[..^unitLength]; - var sizeValue = float.Parse( - number, - NumberStyles.Float | NumberStyles.Number, - provider); - var sizeMode = unit switch - { - "" => ELengthMode.Pixel, - "px" => ELengthMode.Pixel, - "%" => ELengthMode.Percent, - "pt" => ELengthMode.Points, - _ => throw new NotSupportedException($"The unit '{unit}' is not supported.") - }; - if (sizeMode is ELengthMode.Percent) - sizeValue /= 100.0F; - return new Length(sizeValue, sizeMode); + if (!TryParse(s, provider, out var result)) + throw new FormatException($"The given string '{s.ToString()}' is not a valid {nameof(Length)}."); + return result; } /// public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Length result) { - var unitLength = 0; - for (var i = s.Length - 1; i >= 0 && s[i].IsLetter(); i--) - unitLength++; - var unit = s[^unitLength..]; - var number = s[..^unitLength]; - var sizeValue = float.Parse( - number, - NumberStyles.Float | NumberStyles.Number, - provider); - var sizeMode = unit switch + var unitOffset = 0; + for (var i = s.Length - 1; i >= 0 && !s[i].IsDigit() && s[i] is not '.'; i--) + unitOffset++; + var unit = s[^unitOffset..]; + var number = s[..^unitOffset]; + ELengthUnit sizeUnit; + switch (unit) { - "" => ELengthMode.Pixel, - "px" => ELengthMode.Pixel, - "%" => ELengthMode.Percent, - "pt" => ELengthMode.Points, - _ => default(ELengthMode?), - }; - if (sizeMode is null) + case "": + case "px": + case "pX": + case "Px": + case "PX": + sizeUnit = ELengthUnit.Pixel; + break; + case "%": + sizeUnit = ELengthUnit.Percent; + break; + case "pt": + case "pT": + case "Pt": + case "PT": + sizeUnit = ELengthUnit.Points; + break; + case "auto": + case "autO": + case "auTo": + case "auTO": + case "aUto": + case "aUtO": + case "aUTo": + case "aUTO": + case "Auto": + case "AutO": + case "AuTo": + case "AuTO": + case "AUto": + case "AUtO": + case "AUTo": + case "AUTO": + sizeUnit = ELengthUnit.Auto; + break; + case "in": + case "iN": + case "In": + case "IN": + sizeUnit = ELengthUnit.Inches; + break; + case "mm": + case "mM": + case "Mm": + case "MM": + sizeUnit = ELengthUnit.Millimeters; + break; + case "cm": + case "cM": + case "Cm": + case "CM": + sizeUnit = ELengthUnit.Centimeters; + break; + default: + result = default; + return false; + } + + if (sizeUnit is ELengthUnit.Auto) + { + result = new Length(default, ELengthUnit.Auto); + return true; + } + + if (!float.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out var sizeValue)) { result = default; return false; } - if (sizeMode is ELengthMode.Percent) + + if (sizeUnit is ELengthUnit.Percent) sizeValue /= 100.0F; - result = new Length(sizeValue, sizeMode.Value); + result = new Length(sizeValue, sizeUnit); return true; } /// public override string ToString() => ToString(CultureInfo.CurrentCulture); + /// public string ToString(IFormatProvider? provider) { var sizeValue = Value; - var sizeMode = LengthMode; - if (sizeMode is ELengthMode.Percent) + var sizeMode = Unit; + if (sizeMode is ELengthUnit.Percent) sizeValue *= 100.0F; var sizeUnit = sizeMode switch { - ELengthMode.Pixel => "px", - ELengthMode.Percent => "%", - ELengthMode.Points => "pt", - _ => throw new NotSupportedException($"The size mode '{sizeMode}' is not supported."), + ELengthUnit.Pixel => "px", + ELengthUnit.Percent => "%", + ELengthUnit.Points => "pt", + ELengthUnit.Auto => "auto", + ELengthUnit.Millimeters => "mm", + ELengthUnit.Centimeters => "cm", + ELengthUnit.Inches => "in", + _ => throw new NotSupportedException($"The size mode '{sizeMode}' is not supported."), }; - return string.Concat(sizeValue.ToString(provider), sizeUnit); + return sizeMode is ELengthUnit.Auto + ? sizeUnit + : string.Concat(sizeValue.ToString(CultureInfo.InvariantCulture), sizeUnit); } } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Data/Thickness.cs b/source/X39.Solutions.PdfTemplate/Data/Thickness.cs index ab2bccb..56651e7 100644 --- a/source/X39.Solutions.PdfTemplate/Data/Thickness.cs +++ b/source/X39.Solutions.PdfTemplate/Data/Thickness.cs @@ -34,14 +34,22 @@ public Thickness(Length horizontal, Length vertical) : this(horizontal, vertical /// Translates the thickness to a pixels rectangle. /// /// The bounds of the rectangle - /// The DPI to use for the calculation in case of in pixels + /// The DPI to use for the calculation in case of in pixels /// The translated rectangle public Rectangle ToRectangle(Rectangle bounds, float dpi) { - var left = Left.ToPixels(bounds.Width, dpi); - var top = Top.ToPixels(bounds.Height, dpi); - var right = Right.ToPixels(bounds.Width, dpi); - var bottom = Bottom.ToPixels(bounds.Height, dpi); + var left = Left.Unit is ELengthUnit.Auto + ? 0F + : Left.ToPixels(bounds.Width, dpi); + var top = Top.Unit is ELengthUnit.Auto + ? 0F + : Top.ToPixels(bounds.Height, dpi); + var right = Right.Unit is ELengthUnit.Auto + ? 0F + : Right.ToPixels(bounds.Width, dpi); + var bottom = Bottom.Unit is ELengthUnit.Auto + ? 0F + : Bottom.ToPixels(bounds.Height, dpi); return new Rectangle( left, top, @@ -53,19 +61,27 @@ public Rectangle ToRectangle(Rectangle bounds, float dpi) /// Translates the thickness to a pixels rectangle. /// /// The bounds of the rectangle - /// The DPI to use for the calculation in case of in pixels + /// The DPI to use for the calculation in case of in pixels /// The translated rectangle public Rectangle ToRectangle(Size bounds, float dpi) { - var left = Left.ToPixels(bounds.Width, dpi); - var top = Top.ToPixels(bounds.Height, dpi); - var width = Right.ToPixels(bounds.Width, dpi); - var height = Bottom.ToPixels(bounds.Height, dpi); + var left = Left.Unit is ELengthUnit.Auto + ? 0F + : Left.ToPixels(bounds.Width, dpi); + var top = Top.Unit is ELengthUnit.Auto + ? 0F + : Top.ToPixels(bounds.Height, dpi); + var right = Right.Unit is ELengthUnit.Auto + ? 0F + : Right.ToPixels(bounds.Width, dpi); + var bottom = Bottom.Unit is ELengthUnit.Auto + ? 0F + : Bottom.ToPixels(bounds.Height, dpi); return new Rectangle( left, top, - width, - height); + right, + bottom); } /// @@ -117,15 +133,16 @@ public static Thickness Parse(ReadOnlySpan s, IFormatProvider? provider) break; case 4: left = Length.Parse(s[..s.IndexOf(' ')], provider); - s = s[(s.IndexOf(' ') + 1)..]; + s = s[(s.IndexOf(' ') + 1)..]; top = Length.Parse(s[..s.IndexOf(' ')], provider); - s = s[(s.IndexOf(' ') + 1)..]; + s = s[(s.IndexOf(' ') + 1)..]; right = Length.Parse(s[..s.IndexOf(' ')], provider); bottom = Length.Parse(s[(s.IndexOf(' ') + 1)..], provider); break; default: throw new FormatException($"The thickness '{s}' is not in the correct format."); } + return new Thickness(left, top, right, bottom); } diff --git a/source/X39.Solutions.PdfTemplate/Generator.cs b/source/X39.Solutions.PdfTemplate/Generator.cs index 7c9486a..364fc18 100644 --- a/source/X39.Solutions.PdfTemplate/Generator.cs +++ b/source/X39.Solutions.PdfTemplate/Generator.cs @@ -204,7 +204,8 @@ private async Task GenerateAsync( rootNode = await templateReader.ReadAsync(reader, cancellationToken) .ConfigureAwait(false); - var template = Template.Create(rootNode, _controlStorage, cultureInfo); + await using var template = await Template.CreateAsync(rootNode, _controlStorage, cultureInfo, cancellationToken) + .ConfigureAwait(false); var pageSize = new Size( options.DotsPerMillimeter * options.PageWidthInMillimeters, options.DotsPerMillimeter * options.PageHeightInMillimeters); @@ -330,7 +331,7 @@ private async Task GenerateAsync( footerCanvasAbstraction.Render(canvas); canvas.Restore(); currentHeight += bodyPageSize.Height; - + canvas.Restore(); } diff --git a/source/X39.Solutions.PdfTemplate/IInitializeAsync.cs b/source/X39.Solutions.PdfTemplate/IInitializeAsync.cs new file mode 100644 index 0000000..12d9af0 --- /dev/null +++ b/source/X39.Solutions.PdfTemplate/IInitializeAsync.cs @@ -0,0 +1,14 @@ +namespace X39.Solutions.PdfTemplate; + +/// +/// Interface that allows a control to be initialized asynchronously after it and all its children have been created. +/// +[PublicAPI] +public interface IInitializeAsync +{ + /// + /// Initializes the control asynchronously. + /// + /// A that represents the asynchronous operation. + Task InitializeAsync(CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/ServiceCollectionExtensions.cs b/source/X39.Solutions.PdfTemplate/ServiceCollectionExtensions.cs index 71749ad..cb687f7 100644 --- a/source/X39.Solutions.PdfTemplate/ServiceCollectionExtensions.cs +++ b/source/X39.Solutions.PdfTemplate/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using X39.Solutions.PdfTemplate.Services; using X39.Solutions.PdfTemplate.Services.PropertyAccessCache; +using X39.Solutions.PdfTemplate.Services.ResourceResolver; using X39.Solutions.PdfTemplate.Services.TextService; namespace X39.Solutions.PdfTemplate; @@ -15,6 +16,21 @@ public static class ServiceCollectionExtensions /// Adds the services required for the generator to the service collection. /// /// The service collection to add the services to. + /// + /// This method adds the following services: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// If you want to use your own implementation of , you can add it after this method. + /// See MSDN Page + /// for more information about how to work with dependency injection in .NET. + /// public static void AddPdfTemplateServices(this IServiceCollection services) { services.AddSingleton(); @@ -22,5 +38,6 @@ public static void AddPdfTemplateServices(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddScoped(); + services.AddScoped(); } } \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/DefaultResourceResolver.cs b/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/DefaultResourceResolver.cs new file mode 100644 index 0000000..079036f --- /dev/null +++ b/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/DefaultResourceResolver.cs @@ -0,0 +1,31 @@ +namespace X39.Solutions.PdfTemplate.Services.ResourceResolver; + +/// +/// This is the default resource resolver that is used if no other is specified. +/// It will never attempt to resolve any resources linking to the file system or the internet, +/// for security reasons. Use a custom if you want to use +/// other sources than base64 encoded data. +/// +public class DefaultResourceResolver : IResourceResolver +{ + /// + public ValueTask ResolveImageAsync(string source, CancellationToken cancellationToken = default) + { + if (source.StartsWith("data:image/", StringComparison.OrdinalIgnoreCase)) + { + // remove the data:image/...;base64, part + var index = source.IndexOf(',') + 1; + if (index == 0) + throw new NotSupportedException("Invalid uri-style base64 image. Expected data:image/...;base64,..."); + var base64 = source.AsSpan()[index..]; + return new ValueTask(Convert.FromBase64String(base64.ToString())); + } + + if (source.All((c) => char.IsLetterOrDigit(c) || c == '+' || c == '/' || c == '=')) + { + return ValueTask.FromResult(Convert.FromBase64String(source)); + } + + throw new NotSupportedException("Only base64 encoded images are supported by the default resource resolver."); + } +} \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/IResourceResolver.cs b/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/IResourceResolver.cs new file mode 100644 index 0000000..9a4bc31 --- /dev/null +++ b/source/X39.Solutions.PdfTemplate/Services/ResourceResolver/IResourceResolver.cs @@ -0,0 +1,21 @@ +namespace X39.Solutions.PdfTemplate.Services.ResourceResolver; + +/// +/// A service that resolves resources for controls. +/// +public interface IResourceResolver +{ + /// + /// Resolves an image from the given source. + /// + /// + /// The source of the image. + /// + /// + /// The cancellation token to use. + /// + /// + /// A that resolves to the image data. + /// + ValueTask ResolveImageAsync(string source, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/source/X39.Solutions.PdfTemplate/Template.cs b/source/X39.Solutions.PdfTemplate/Template.cs index 9b974ab..ee75dbc 100644 --- a/source/X39.Solutions.PdfTemplate/Template.cs +++ b/source/X39.Solutions.PdfTemplate/Template.cs @@ -3,9 +3,9 @@ namespace X39.Solutions.PdfTemplate; -internal class Template +internal sealed class Template : IAsyncDisposable { - public Template( + private Template( IReadOnlyCollection headerControls, IReadOnlyCollection bodyControls, IReadOnlyCollection footerControls) @@ -20,25 +20,58 @@ public Template( public IReadOnlyCollection FooterControls { get; } - public static Template Create(XmlNodeInformation rootNode, ControlStorage cache, CultureInfo cultureInfo) + public static async Task