diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs index 707dd34c48..68e99bdc5f 100644 --- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs +++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs @@ -143,6 +143,9 @@ public class BmpMetadata : IFormatMetadata public FormatConnectingMetadata ToFormatConnectingMetadata() => new() { + EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, PixelTypeInfo = this.GetPixelTypeInfo() }; diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index 3018ec6bf9..a8a51878e0 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -15,16 +15,30 @@ internal sealed class CurDecoderCore : IconDecoderCore } protected override void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, ReadOnlyMemory? colorTable) { - CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); + CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); curFrameMetadata.FromIconDirEntry(entry); curFrameMetadata.Compression = compression; curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; curFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + CurMetadata curMetadata = imageMetadata.GetCurMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + curMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; + curMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; + curMetadata.HotspotX = curFrameMetadata.HotspotX; + curMetadata.HotspotY = curFrameMetadata.HotspotY; + } } } diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index 014944ba69..06cf426dc4 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// /// IcoFrameMetadata. /// -public class CurFrameMetadata : IDeepCloneable, IDeepCloneable +public class CurFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -60,7 +60,7 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable /// Gets or sets the number of bits per pixel.
/// Used when is ///
- public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// /// Gets or sets the color table, if any. @@ -69,11 +69,75 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable public ReadOnlyMemory? ColorTable { get; set; } /// - public CurFrameMetadata DeepClone() => new(this); + public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new CurFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + byte encodingWidth = metadata.EncodingWidth switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingWidth, + _ => 0 + }; + + byte encodingHeight = metadata.EncodingHeight switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingHeight, + _ => 0 + }; + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = encodingWidth, + EncodingHeight = encodingHeight, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable, + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + /// + public CurFrameMetadata DeepClone() => new(this); + internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; @@ -84,7 +148,7 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable internal IconDirEntry ToIconDirEntry() { - byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); @@ -97,4 +161,65 @@ public class CurFrameMetadata : IDeepCloneable, IDeepCloneable ColorCount = colorCount }; } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } } diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index 5c3486d4a2..6e97a8584a 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs @@ -1,16 +1,183 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Provides Ico specific metadata information for the image. +/// Provides Cur specific metadata information for the image. /// -public class CurMetadata : IDeepCloneable, IDeepCloneable +public class CurMetadata : IFormatMetadata { + /// + /// Initializes a new instance of the class. + /// + public CurMetadata() + { + } + + private CurMetadata(CurMetadata other) + { + this.Compression = other.Compression; + this.HotspotX = other.HotspotX; + this.HotspotY = other.HotspotY; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame. + /// + public ushort HotspotX { get; set; } + + /// + /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame. + /// + public ushort HotspotY { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ public byte EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ public byte EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + /// - public CurMetadata DeepClone() => new(); + public static CurMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new CurMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public CurMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Cur/MetadataExtensions.cs b/src/ImageSharp/Formats/Cur/MetadataExtensions.cs deleted file mode 100644 index 6394c564b0..0000000000 --- a/src/ImageSharp/Formats/Cur/MetadataExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Cur; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the Icon format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static CurMetadata GetCurMetadata(this ImageMetadata source) - => source.GetFormatMetadata(CurFormat.Instance); - - /// - /// Gets the Icon format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) - => source.GetFormatMetadata(CurFormat.Instance); - - /// - /// Gets the Icon format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified frame, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the Icon frame metadata exists; otherwise, . - /// - public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata) - => source.TryGetFormatMetadata(CurFormat.Instance, out metadata); -} diff --git a/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs index 31555afe34..ded220c9ad 100644 --- a/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats; /// @@ -9,27 +11,44 @@ namespace SixLabors.ImageSharp.Formats; public class FormatConnectingFrameMetadata { /// - /// Gets or sets the frame color table. + /// Gets information about the encoded pixel type if any. + /// + public PixelTypeInfo? PixelTypeInfo { get; init; } + + /// + /// Gets the frame color table if any. + /// + public ReadOnlyMemory? ColorTable { get; init; } + + /// + /// Gets the frame color table mode. + /// + public FrameColorTableMode ColorTableMode { get; init; } + + /// + /// Gets the duration of the frame. /// - public ReadOnlyMemory? ColorTable { get; set; } + public TimeSpan Duration { get; init; } /// - /// Gets or sets the frame color table mode. + /// Gets the frame alpha blending mode. /// - public FrameColorTableMode ColorTableMode { get; set; } + public FrameBlendMode BlendMode { get; init; } /// - /// Gets or sets the duration of the frame. + /// Gets the frame disposal mode. /// - public TimeSpan Duration { get; set; } + public FrameDisposalMode DisposalMode { get; init; } /// - /// Gets or sets the frame alpha blending mode. + /// Gets or sets the encoding width.
+ /// Used for formats that require a specific frame size. ///
- public FrameBlendMode BlendMode { get; set; } + public int? EncodingWidth { get; set; } /// - /// Gets or sets the frame disposal mode. + /// Gets or sets the encoding height.
+ /// Used for formats that require a specific frame size. ///
- public FrameDisposalMode DisposalMode { get; set; } + public int? EncodingHeight { get; set; } } diff --git a/src/ImageSharp/Formats/FormatConnectingMetadata.cs b/src/ImageSharp/Formats/FormatConnectingMetadata.cs index 07579c09e8..baf0a35457 100644 --- a/src/ImageSharp/Formats/FormatConnectingMetadata.cs +++ b/src/ImageSharp/Formats/FormatConnectingMetadata.cs @@ -29,7 +29,7 @@ public class FormatConnectingMetadata public PixelTypeInfo PixelTypeInfo { get; init; } /// - /// Gets the shared color table. + /// Gets the shared color table if any. /// public ReadOnlyMemory? ColorTable { get; init; } diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index f4990c66af..8b59974eb3 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -15,16 +15,28 @@ internal sealed class IcoDecoderCore : IconDecoderCore } protected override void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, ReadOnlyMemory? colorTable) { - IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); + IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); icoFrameMetadata.FromIconDirEntry(entry); icoFrameMetadata.Compression = compression; icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; icoFrameMetadata.ColorTable = colorTable; + + if (index == 0) + { + IcoMetadata curMetadata = imageMetadata.GetIcoMetadata(); + curMetadata.Compression = compression; + curMetadata.BmpBitsPerPixel = bitsPerPixel; + curMetadata.ColorTable = colorTable; + curMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; + curMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; + } } } diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index ea27d13c8d..c244e38981 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// /// Provides Ico specific metadata information for the image frame. /// -public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable +public class IcoFrameMetadata : IFormatFrameMetadata { /// /// Initializes a new instance of the class. @@ -53,7 +53,7 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable /// Gets or sets the number of bits per pixel.
/// Used when is ///
- public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// /// Gets or sets the color table, if any. @@ -62,11 +62,75 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable public ReadOnlyMemory? ColorTable { get; set; } /// - public IcoFrameMetadata DeepClone() => new(this); + public static IcoFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + if (!metadata.PixelTypeInfo.HasValue) + { + return new IcoFrameMetadata + { + BmpBitsPerPixel = BmpBitsPerPixel.Bit32, + Compression = IconFrameCompression.Png + }; + } + + byte encodingWidth = metadata.EncodingWidth switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingWidth, + _ => 0 + }; + + byte encodingHeight = metadata.EncodingHeight switch + { + > 255 => 0, + <= 255 and >= 1 => (byte)metadata.EncodingHeight, + _ => 0 + }; + + int bpp = metadata.PixelTypeInfo.Value.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoFrameMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + EncodingWidth = encodingWidth, + EncodingHeight = encodingHeight, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + => new() + { + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable, + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + /// + public IcoFrameMetadata DeepClone() => new(this); + internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; @@ -75,7 +139,7 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable internal IconDirEntry ToIconDirEntry() { - byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8 + byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 ? (byte)0 : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); @@ -92,4 +156,65 @@ public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable }, }; } + + private PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } } diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index f165bf9164..7e31468ecc 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -1,16 +1,171 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Ico; /// /// Provides Ico specific metadata information for the image. /// -public class IcoMetadata : IDeepCloneable, IDeepCloneable +public class IcoMetadata : IFormatMetadata { + /// + /// Initializes a new instance of the class. + /// + public IcoMetadata() + { + } + + private IcoMetadata(IcoMetadata other) + { + this.Compression = other.Compression; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame compressions format. Derived from the root frame. + /// + public IconFrameCompression Compression { get; set; } + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ public byte EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame. + ///
+ public byte EncodingHeight { get; set; } + + /// + /// Gets or sets the number of bits per pixel.
+ /// Used when is + ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the color table, if any. Derived from the root frame.
+ /// The underlying pixel format is represented by . + ///
+ public ReadOnlyMemory? ColorTable { get; set; } + + /// + public static IcoMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int bpp = metadata.PixelTypeInfo.BitsPerPixel; + BmpBitsPerPixel bbpp = bpp switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + IconFrameCompression compression = IconFrameCompression.Bmp; + if (bbpp is BmpBitsPerPixel.Bit32) + { + compression = IconFrameCompression.Png; + } + + return new IcoMetadata + { + BmpBitsPerPixel = bbpp, + Compression = compression, + ColorTable = compression == IconFrameCompression.Bmp ? metadata.ColorTable : null + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bpp = (int)this.BmpBitsPerPixel; + PixelComponentInfo info; + PixelColorType color; + PixelAlphaRepresentation alpha = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bpp = 32; + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + info = PixelComponentInfo.Create(1, bpp, 1); + color = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + info = PixelComponentInfo.Create(1, bpp, 2); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + info = PixelComponentInfo.Create(1, bpp, 4); + color = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + info = PixelComponentInfo.Create(1, bpp, 8); + color = PixelColorType.Indexed; + break; + + // Could be 555 with padding but 565 is more common in newer bitmaps and offers + // greater accuracy due to extra green precision. + case BmpBitsPerPixel.Bit16: + info = PixelComponentInfo.Create(3, bpp, 5, 6, 5); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + info = PixelComponentInfo.Create(3, bpp, 8, 8, 8); + color = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + info = PixelComponentInfo.Create(4, bpp, 8, 8, 8, 8); + color = PixelColorType.RGB | PixelColorType.Alpha; + alpha = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bpp) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + /// - public IcoMetadata DeepClone() => new(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8 + ? EncodingType.Lossy + : EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo(), + ColorTable = this.ColorTable + }; /// IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public IcoMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Ico/MetadataExtensions.cs b/src/ImageSharp/Formats/Ico/MetadataExtensions.cs deleted file mode 100644 index 497375f994..0000000000 --- a/src/ImageSharp/Formats/Ico/MetadataExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Ico; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the Ico format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static IcoMetadata GetIcoMetadata(this ImageMetadata source) - => source.GetFormatMetadata(IcoFormat.Instance); - - /// - /// Gets the Ico format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// The . - public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) - => source.GetFormatMetadata(IcoFormat.Instance); - - /// - /// Gets the Ico format specific metadata for the image frame. - /// - /// The metadata this method extends. - /// - /// When this method returns, contains the metadata associated with the specified frame, - /// if found; otherwise, the default value for the type of the metadata parameter. - /// This parameter is passed uninitialized. - /// - /// - /// if the Ico frame metadata exists; otherwise, . - /// - public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata) - => source.TryGetFormatMetadata(IcoFormat.Instance, out metadata); -} diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index 74fe7b9e50..0feed7f693 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -74,7 +74,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals PngMetadata? pngMetadata = null; Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; ReadOnlyMemory? colorTable = null; ImageFrame target = new(this.Options.Configuration, this.Dimensions); ImageFrame source = x.Image.Frames.RootFrameUnsafe; @@ -106,7 +106,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals } this.SetFrameMetadata( + metadata, target.Metadata, + x.Index, this.entries[x.Index], x.Compression, bitsPerPixel, @@ -146,7 +148,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals int bpp = 0; for (int i = 0; i < frames.Length; i++) { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; ReadOnlyMemory? colorTable = null; ref IconDirEntry entry = ref this.entries[i]; @@ -198,7 +200,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals frames[i] = frameMetadata; this.SetFrameMetadata( + metadata, frames[i], + i, this.entries[i], isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, bitsPerPixel, @@ -220,11 +224,13 @@ internal abstract class IconDecoderCore : IImageDecoderInternals metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); } - return new(new(bpp), this.Dimensions, metadata, frames); + return new(this.Dimensions, metadata, frames); } protected abstract void SetFrameMetadata( - ImageFrameMetadata metadata, + ImageMetadata imageMetadata, + ImageFrameMetadata frameMetadata, + int index, in IconDirEntry entry, IconFrameCompression compression, BmpBitsPerPixel bitsPerPixel, diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 2433396612..509c9f420c 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -167,7 +167,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals { this.Compression = compression; this.BmpBitsPerPixel = compression == IconFrameCompression.Png - ? BmpBitsPerPixel.Pixel32 + ? BmpBitsPerPixel.Bit32 : bmpBitsPerPixel; this.ColorTable = colorTable; this.iconDirEntry = iconDirEntry; diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index 5d7f84acfb..73d1145883 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -3,7 +3,9 @@ // using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -121,6 +123,108 @@ public static partial class ImageExtensions encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path) => SaveAsCur(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path) => SaveAsCurAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsCurAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsCur(this Image source, string path, CurEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, string path, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream) + => SaveAsCur(source, stream, default); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsCurAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsCur(this Image source, Stream stream, CurEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance)); + + /// + /// Saves the image to the given stream with the Cur format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsCurAsync(this Image source, Stream stream, CurEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Gif format. /// @@ -223,6 +327,108 @@ public static partial class ImageExtensions encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path) => SaveAsIco(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path) => SaveAsIcoAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsIcoAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsIco(this Image source, string path, IcoEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, string path, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream) + => SaveAsIco(source, stream, default); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsIcoAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsIco(this Image source, Stream stream, IcoEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ico format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsIcoAsync(this Image source, Stream stream, IcoEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Jpeg format. /// diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index 826f5905b1..e35d00ed39 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -4,7 +4,9 @@ // using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -40,6 +42,26 @@ public static class ImageMetadataExtensions /// The new public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static CurMetadata GetCurMetadata(this ImageMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static CurMetadata CloneCurMetadata(this ImageMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -60,6 +82,26 @@ public static class ImageMetadataExtensions /// The new public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static IcoMetadata GetIcoMetadata(this ImageMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -201,6 +243,46 @@ public static class ImageMetadataExtensions public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude index 24ac66a70b..89940d406e 100644 --- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -5,7 +5,9 @@ <#+ private static readonly string[] formats = new []{ "Bmp", + "Cur", "Gif", + "Ico", "Jpeg", "Pbm", "Png", @@ -16,6 +18,8 @@ }; private static readonly string[] frameFormats = new []{ + "Cur", + "Ico", "Gif", "Png", "Tiff", diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index bcc7484b3b..9c0de1edbe 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -109,7 +109,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable if (this.DecodedImageFormat is not null && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) { - return TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + TFormatFrameMetadata derivedMeta = TFormatFrameMetadata.FromFormatConnectingFrameMetadata(decodedMetadata.ToFormatConnectingFrameMetadata()); + this.formatMetadata[key] = derivedMeta; + return derivedMeta; } TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata(); @@ -119,7 +121,7 @@ public sealed class ImageFrameMetadata : IDeepCloneable internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value) where TFormatMetadata : class - where TFormatFrameMetadata : class, IDeepCloneable + where TFormatFrameMetadata : class, IFormatFrameMetadata => this.formatMetadata[key] = value; /// diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 700c7fe83c..b5c46d7582 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -194,7 +194,9 @@ public sealed class ImageMetadata : IDeepCloneable if (this.DecodedImageFormat is not null && this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) { - return TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + TFormatMetadata derivedMeta = TFormatMetadata.FromFormatConnectingMetadata(decodedMetadata.ToFormatConnectingMetadata()); + this.formatMetadata[key] = derivedMeta; + return derivedMeta; } // Fall back to a default instance. @@ -217,7 +219,7 @@ public sealed class ImageMetadata : IDeepCloneable => ((IDeepCloneable)this.GetFormatMetadata(key)).DeepClone(); internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value) - where TFormatMetadata : class, IDeepCloneable + where TFormatMetadata : class, IFormatMetadata => this.formatMetadata[key] = value; /// diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index 4efd336482..f7ee7614af 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -23,7 +23,7 @@ public class CurDecoderTests Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -36,6 +36,6 @@ public class CurDecoderTests Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index b9b66296df..59c40c9245 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -2,9 +2,11 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; +using static SixLabors.ImageSharp.Tests.TestImages.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; @@ -17,7 +19,7 @@ public class CurEncoderTests [WithFile(CurReal, PixelTypes.Rgba32)] [WithFile(WindowsMouse, PixelTypes.Rgba32)] public void CanRoundTripEncoder(TestImageProvider provider) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(CurDecoder.Instance); using MemoryStream memStream = new(); @@ -31,4 +33,34 @@ public class CurEncoderTests encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); } + + [Theory] + [WithFile(Flutter, PixelTypes.Rgba32)] + public void CanConvertFromIco(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(IcoDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, Encoder); + memStream.Seek(0, SeekOrigin.Begin); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider); + + // Despite preservation of the palette. The process can still be lossy + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); + + for (int i = 0; i < image.Frames.Count; i++) + { + IcoFrameMetadata icoFrame = image.Frames[i].Metadata.GetIcoMetadata(); + CurFrameMetadata curFrame = encoded.Frames[i].Metadata.GetCurMetadata(); + + // Compression may differ as we cannot convert that. + // Color table may differ. + Assert.Equal(icoFrame.BmpBitsPerPixel, curFrame.BmpBitsPerPixel); + Assert.Equal(icoFrame.EncodingWidth, curFrame.EncodingWidth); + Assert.Equal(icoFrame.EncodingHeight, curFrame.EncodingHeight); + } + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index a776a637b8..bc46df0955 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -56,7 +56,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit1, meta.BmpBitsPerPixel); } [Theory] @@ -92,7 +92,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit24, meta.BmpBitsPerPixel); } [Theory] @@ -128,7 +128,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -163,7 +163,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit4, meta.BmpBitsPerPixel); } [Theory] @@ -199,7 +199,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit8, meta.BmpBitsPerPixel); } [Theory] @@ -229,7 +229,7 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Png, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } [Theory] @@ -262,7 +262,7 @@ public class IcoDecoderTests { ImageFrame frame = image.Frames[i]; IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata(); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } image.DebugSaveMultiFrame(provider); @@ -327,6 +327,6 @@ public class IcoDecoderTests Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(IconFrameCompression.Bmp, meta.Compression); - Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); + Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index db28f9f703..751db384d7 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using static SixLabors.ImageSharp.Tests.TestImages.Cur; using static SixLabors.ImageSharp.Tests.TestImages.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; @@ -26,9 +28,38 @@ public class IcoEncoderTests memStream.Seek(0, SeekOrigin.Begin); using Image encoded = Image.Load(memStream); - encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); + encoded.DebugSaveMultiFrame(provider); // Despite preservation of the palette. The process can still be lossy encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); } + + [Theory] + [WithFile(WindowsMouse, PixelTypes.Rgba32)] + public void CanConvertFromCur(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(CurDecoder.Instance); + using MemoryStream memStream = new(); + + image.Save(memStream, Encoder); + memStream.Seek(0, SeekOrigin.Begin); + + using Image encoded = Image.Load(memStream); + encoded.DebugSaveMultiFrame(provider); + + encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); + + for (int i = 0; i < image.Frames.Count; i++) + { + CurFrameMetadata curFrame = image.Frames[i].Metadata.GetCurMetadata(); + IcoFrameMetadata icoFrame = encoded.Frames[i].Metadata.GetIcoMetadata(); + + // Compression may differ as we cannot convert that. + Assert.Equal(curFrame.BmpBitsPerPixel, icoFrame.BmpBitsPerPixel); + Assert.Equal(curFrame.EncodingWidth, icoFrame.EncodingWidth); + Assert.Equal(curFrame.EncodingHeight, icoFrame.EncodingHeight); + Assert.Equal(curFrame.ColorTable, icoFrame.ColorTable); + } + } }