Browse Source

Migrate ICO and CUR

pull/2751/head
James Jackson-South 2 years ago
parent
commit
9bdbc526da
  1. 3
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  2. 18
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  3. 133
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  4. 173
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  5. 45
      src/ImageSharp/Formats/Cur/MetadataExtensions.cs
  6. 39
      src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs
  7. 2
      src/ImageSharp/Formats/FormatConnectingMetadata.cs
  8. 16
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  9. 133
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  10. 159
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  11. 45
      src/ImageSharp/Formats/Ico/MetadataExtensions.cs
  12. 14
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  13. 2
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  14. 206
      src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
  15. 82
      src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
  16. 4
      src/ImageSharp/Formats/_Generated/_Formats.ttinclude
  17. 6
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  18. 6
      src/ImageSharp/Metadata/ImageMetadata.cs
  19. 4
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  20. 34
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
  21. 16
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  22. 33
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs

3
src/ImageSharp/Formats/Bmp/BmpMetadata.cs

@ -143,6 +143,9 @@ public class BmpMetadata : IFormatMetadata<BmpMetadata>
public FormatConnectingMetadata ToFormatConnectingMetadata() public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new() => new()
{ {
EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo() PixelTypeInfo = this.GetPixelTypeInfo()
}; };

18
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -15,16 +15,30 @@ internal sealed class CurDecoderCore : IconDecoderCore
} }
protected override void SetFrameMetadata( protected override void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? colorTable) ReadOnlyMemory<Color>? colorTable)
{ {
CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata(); CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata();
curFrameMetadata.FromIconDirEntry(entry); curFrameMetadata.FromIconDirEntry(entry);
curFrameMetadata.Compression = compression; curFrameMetadata.Compression = compression;
curFrameMetadata.BmpBitsPerPixel = bitsPerPixel; curFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
curFrameMetadata.ColorTable = colorTable; 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;
}
} }
} }

133
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary> /// <summary>
/// IcoFrameMetadata. /// IcoFrameMetadata.
/// </summary> /// </summary>
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class. /// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
@ -60,7 +60,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary> /// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary> /// <summary>
/// Gets or sets the color table, if any. /// Gets or sets the color table, if any.
@ -69,11 +69,75 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
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
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry) internal void FromIconDirEntry(IconDirEntry entry)
{ {
this.EncodingWidth = entry.Width; this.EncodingWidth = entry.Width;
@ -84,7 +148,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
internal IconDirEntry ToIconDirEntry() 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)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -97,4 +161,65 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
ColorCount = colorCount 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
};
}
} }

173
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -1,16 +1,183 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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; namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image. /// Provides Cur specific metadata information for the image.
/// </summary> /// </summary>
public class CurMetadata : IDeepCloneable<CurMetadata>, IDeepCloneable public class CurMetadata : IFormatMetadata<CurMetadata>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="CurMetadata"/> class.
/// </summary>
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();
}
}
/// <summary>
/// Gets or sets the frame compressions format. Derived from the root frame.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left. Derived from the root frame.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top. Derived from the root frame.
/// </summary>
public ushort HotspotY { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
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
};
}
/// <inheritdoc/>
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
};
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
EncodingType = this.Compression == IconFrameCompression.Bmp && this.BmpBitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurMetadata DeepClone() => new(this);
} }

45
src/ImageSharp/Formats/Cur/MetadataExtensions.cs

@ -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;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static partial class MetadataExtensions
{
/// <summary>
/// Gets the Icon format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="CurMetadata"/>.</returns>
public static CurMetadata GetCurMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(CurFormat.Instance);
/// <summary>
/// Gets the Icon format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="CurFrameMetadata"/>.</returns>
public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source)
=> source.GetFormatMetadata(CurFormat.Instance);
/// <summary>
/// Gets the Icon format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">
/// 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.
/// </param>
/// <returns>
/// <see langword="true"/> if the Icon frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata)
=> source.TryGetFormatMetadata(CurFormat.Instance, out metadata);
}

39
src/ImageSharp/Formats/FormatConnectingFrameMetadata.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats; namespace SixLabors.ImageSharp.Formats;
/// <summary> /// <summary>
@ -9,27 +11,44 @@ namespace SixLabors.ImageSharp.Formats;
public class FormatConnectingFrameMetadata public class FormatConnectingFrameMetadata
{ {
/// <summary> /// <summary>
/// Gets or sets the frame color table. /// Gets information about the encoded pixel type if any.
/// </summary>
public PixelTypeInfo? PixelTypeInfo { get; init; }
/// <summary>
/// Gets the frame color table if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; }
/// <summary>
/// Gets the frame color table mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; init; }
/// <summary>
/// Gets the duration of the frame.
/// </summary> /// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; } public TimeSpan Duration { get; init; }
/// <summary> /// <summary>
/// Gets or sets the frame color table mode. /// Gets the frame alpha blending mode.
/// </summary> /// </summary>
public FrameColorTableMode ColorTableMode { get; set; } public FrameBlendMode BlendMode { get; init; }
/// <summary> /// <summary>
/// Gets or sets the duration of the frame. /// Gets the frame disposal mode.
/// </summary> /// </summary>
public TimeSpan Duration { get; set; } public FrameDisposalMode DisposalMode { get; init; }
/// <summary> /// <summary>
/// Gets or sets the frame alpha blending mode. /// Gets or sets the encoding width. <br />
/// Used for formats that require a specific frame size.
/// </summary> /// </summary>
public FrameBlendMode BlendMode { get; set; } public int? EncodingWidth { get; set; }
/// <summary> /// <summary>
/// Gets or sets the frame disposal mode. /// Gets or sets the encoding height. <br />
/// Used for formats that require a specific frame size.
/// </summary> /// </summary>
public FrameDisposalMode DisposalMode { get; set; } public int? EncodingHeight { get; set; }
} }

2
src/ImageSharp/Formats/FormatConnectingMetadata.cs

@ -29,7 +29,7 @@ public class FormatConnectingMetadata
public PixelTypeInfo PixelTypeInfo { get; init; } public PixelTypeInfo PixelTypeInfo { get; init; }
/// <summary> /// <summary>
/// Gets the shared color table. /// Gets the shared color table if any.
/// </summary> /// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; init; } public ReadOnlyMemory<Color>? ColorTable { get; init; }

16
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -15,16 +15,28 @@ internal sealed class IcoDecoderCore : IconDecoderCore
} }
protected override void SetFrameMetadata( protected override void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? colorTable) ReadOnlyMemory<Color>? colorTable)
{ {
IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata(); IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata();
icoFrameMetadata.FromIconDirEntry(entry); icoFrameMetadata.FromIconDirEntry(entry);
icoFrameMetadata.Compression = compression; icoFrameMetadata.Compression = compression;
icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel; icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
icoFrameMetadata.ColorTable = colorTable; 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;
}
} }
} }

133
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image frame. /// Provides Ico specific metadata information for the image frame.
/// </summary> /// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class. /// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
@ -53,7 +53,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
/// Gets or sets the number of bits per pixel.<br/> /// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/> /// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary> /// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32; public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary> /// <summary>
/// Gets or sets the color table, if any. /// Gets or sets the color table, if any.
@ -62,11 +62,75 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; } public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
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
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
=> new()
{
PixelTypeInfo = this.GetPixelTypeInfo(),
ColorTable = this.ColorTable,
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry) internal void FromIconDirEntry(IconDirEntry entry)
{ {
this.EncodingWidth = entry.Width; this.EncodingWidth = entry.Width;
@ -75,7 +139,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
internal IconDirEntry ToIconDirEntry() 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)0
: (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel); : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
@ -92,4 +156,65 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, 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
};
}
} }

159
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -1,16 +1,171 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // 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; namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary> /// <summary>
/// Provides Ico specific metadata information for the image. /// Provides Ico specific metadata information for the image.
/// </summary> /// </summary>
public class IcoMetadata : IDeepCloneable<IcoMetadata>, IDeepCloneable public class IcoMetadata : IFormatMetadata<IcoMetadata>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="IcoMetadata"/> class.
/// </summary>
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();
}
}
/// <summary>
/// Gets or sets the frame compressions format. Derived from the root frame.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. Derived from the root frame.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
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
};
}
/// <inheritdoc/>
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
};
}
/// <inheritdoc/> /// <inheritdoc/>
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
};
/// <inheritdoc/> /// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoMetadata DeepClone() => new(this);
} }

45
src/ImageSharp/Formats/Ico/MetadataExtensions.cs

@ -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;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static partial class MetadataExtensions
{
/// <summary>
/// Gets the Ico format specific metadata for the image.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="IcoMetadata"/>.</returns>
public static IcoMetadata GetIcoMetadata(this ImageMetadata source)
=> source.GetFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the Ico format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <returns>The <see cref="IcoFrameMetadata"/>.</returns>
public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source)
=> source.GetFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the Ico format specific metadata for the image frame.
/// </summary>
/// <param name="source">The metadata this method extends.</param>
/// <param name="metadata">
/// 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.
/// </param>
/// <returns>
/// <see langword="true"/> if the Ico frame metadata exists; otherwise, <see langword="false"/>.
/// </returns>
public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata)
=> source.TryGetFormatMetadata(IcoFormat.Instance, out metadata);
}

14
src/ImageSharp/Formats/Icon/IconDecoderCore.cs

@ -74,7 +74,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
PngMetadata? pngMetadata = null; PngMetadata? pngMetadata = null;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{ {
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null; ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions); ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe; ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe;
@ -106,7 +106,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
} }
this.SetFrameMetadata( this.SetFrameMetadata(
metadata,
target.Metadata, target.Metadata,
x.Index,
this.entries[x.Index], this.entries[x.Index],
x.Compression, x.Compression,
bitsPerPixel, bitsPerPixel,
@ -146,7 +148,7 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
int bpp = 0; int bpp = 0;
for (int i = 0; i < frames.Length; i++) for (int i = 0; i < frames.Length; i++)
{ {
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32; BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null; ReadOnlyMemory<Color>? colorTable = null;
ref IconDirEntry entry = ref this.entries[i]; ref IconDirEntry entry = ref this.entries[i];
@ -198,7 +200,9 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
frames[i] = frameMetadata; frames[i] = frameMetadata;
this.SetFrameMetadata( this.SetFrameMetadata(
metadata,
frames[i], frames[i],
i,
this.entries[i], this.entries[i],
isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp,
bitsPerPixel, bitsPerPixel,
@ -220,11 +224,13 @@ internal abstract class IconDecoderCore : IImageDecoderInternals
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
} }
return new(new(bpp), this.Dimensions, metadata, frames); return new(this.Dimensions, metadata, frames);
} }
protected abstract void SetFrameMetadata( protected abstract void SetFrameMetadata(
ImageFrameMetadata metadata, ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry, in IconDirEntry entry,
IconFrameCompression compression, IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel, BmpBitsPerPixel bitsPerPixel,

2
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -167,7 +167,7 @@ internal abstract class IconEncoderCore : IImageEncoderInternals
{ {
this.Compression = compression; this.Compression = compression;
this.BmpBitsPerPixel = compression == IconFrameCompression.Png this.BmpBitsPerPixel = compression == IconFrameCompression.Png
? BmpBitsPerPixel.Pixel32 ? BmpBitsPerPixel.Bit32
: bmpBitsPerPixel; : bmpBitsPerPixel;
this.ColorTable = colorTable; this.ColorTable = colorTable;
this.iconDirEntry = iconDirEntry; this.iconDirEntry = iconDirEntry;

206
src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs

@ -3,7 +3,9 @@
// <auto-generated /> // <auto-generated />
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -121,6 +123,108 @@ public static partial class ImageExtensions
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance),
cancellationToken); cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsCur(this Image source, string path) => SaveAsCur(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsCurAsync(this Image source, string path) => SaveAsCurAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsCurAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsCurAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsCur(this Image source, string path, CurEncoder encoder) =>
source.Save(
path,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsCur(this Image source, Stream stream)
=> SaveAsCur(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsCurAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsCurAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsCur(this Image source, Stream stream, CurEncoder encoder)
=> source.Save(
stream,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(CurFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Cur format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary> /// <summary>
/// Saves the image to the given stream with the Gif format. /// Saves the image to the given stream with the Gif format.
/// </summary> /// </summary>
@ -223,6 +327,108 @@ public static partial class ImageExtensions
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance),
cancellationToken); cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsIco(this Image source, string path) => SaveAsIco(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsIcoAsync(this Image source, string path) => SaveAsIcoAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsIcoAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsIcoAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsIco(this Image source, string path, IcoEncoder encoder) =>
source.Save(
path,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsIco(this Image source, Stream stream)
=> SaveAsIco(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsIcoAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsIcoAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsIco(this Image source, Stream stream, IcoEncoder encoder)
=> source.Save(
stream,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(IcoFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Ico format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
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);
/// <summary> /// <summary>
/// Saves the image to the given stream with the Jpeg format. /// Saves the image to the given stream with the Jpeg format.
/// </summary> /// </summary>

82
src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs

@ -4,7 +4,9 @@
// <auto-generated /> // <auto-generated />
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -40,6 +42,26 @@ public static class ImageMetadataExtensions
/// <returns>The new <see cref="BmpMetadata"/></returns> /// <returns>The new <see cref="BmpMetadata"/></returns>
public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance); public static BmpMetadata CloneBmpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(BmpFormat.Instance);
/// <summary>
/// Gets the <see cref="CurMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="CurMetadata"/>
/// </returns>
public static CurMetadata GetCurMetadata(this ImageMetadata source) => source.GetFormatMetadata(CurFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="CurMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetCurMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="CurMetadata"/></returns>
public static CurMetadata CloneCurMetadata(this ImageMetadata source) => source.CloneFormatMetadata(CurFormat.Instance);
/// <summary> /// <summary>
/// Gets the <see cref="GifMetadata"/> from <paramref name="source"/>.<br/> /// Gets the <see cref="GifMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// 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
/// <returns>The new <see cref="GifMetadata"/></returns> /// <returns>The new <see cref="GifMetadata"/></returns>
public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance); public static GifMetadata CloneGifMetadata(this ImageMetadata source) => source.CloneFormatMetadata(GifFormat.Instance);
/// <summary>
/// Gets the <see cref="IcoMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="IcoMetadata"/>
/// </returns>
public static IcoMetadata GetIcoMetadata(this ImageMetadata source) => source.GetFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetIcoMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="IcoMetadata"/></returns>
public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary> /// <summary>
/// Gets the <see cref="JpegMetadata"/> from <paramref name="source"/>.<br/> /// Gets the <see cref="JpegMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// 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); public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance);
/// <summary>
/// Gets the <see cref="CurFrameMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>
/// The <see cref="CurFrameMetadata"/>
/// </returns>
public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="CurMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetCurMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="CurFrameMetadata"/></returns>
public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance);
/// <summary>
/// Gets the <see cref="IcoFrameMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>
/// The <see cref="IcoFrameMetadata"/>
/// </returns>
public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetIcoMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="IcoFrameMetadata"/></returns>
public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary> /// <summary>
/// Gets the <see cref="GifFrameMetadata"/> from <paramref name="source"/>.<br/> /// Gets the <see cref="GifFrameMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// If none is found, an instance is created either by conversion from the decoded image format metadata

4
src/ImageSharp/Formats/_Generated/_Formats.ttinclude

@ -5,7 +5,9 @@
<#+ <#+
private static readonly string[] formats = new []{ private static readonly string[] formats = new []{
"Bmp", "Bmp",
"Cur",
"Gif", "Gif",
"Ico",
"Jpeg", "Jpeg",
"Pbm", "Pbm",
"Png", "Png",
@ -16,6 +18,8 @@
}; };
private static readonly string[] frameFormats = new []{ private static readonly string[] frameFormats = new []{
"Cur",
"Ico",
"Gif", "Gif",
"Png", "Png",
"Tiff", "Tiff",

6
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -109,7 +109,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
if (this.DecodedImageFormat is not null if (this.DecodedImageFormat is not null
&& this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatFrameMetadata? decodedMetadata)) && 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(); TFormatFrameMetadata newMeta = key.CreateDefaultFormatFrameMetadata();
@ -119,7 +121,7 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value) internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
where TFormatMetadata : class where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata>
=> this.formatMetadata[key] = value; => this.formatMetadata[key] = value;
/// <summary> /// <summary>

6
src/ImageSharp/Metadata/ImageMetadata.cs

@ -194,7 +194,9 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
if (this.DecodedImageFormat is not null if (this.DecodedImageFormat is not null
&& this.formatMetadata.TryGetValue(this.DecodedImageFormat, out IFormatMetadata? decodedMetadata)) && 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. // Fall back to a default instance.
@ -217,7 +219,7 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
=> ((IDeepCloneable<TFormatMetadata>)this.GetFormatMetadata(key)).DeepClone(); => ((IDeepCloneable<TFormatMetadata>)this.GetFormatMetadata(key)).DeepClone();
internal void SetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> key, TFormatMetadata value) internal void SetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> key, TFormatMetadata value)
where TFormatMetadata : class, IDeepCloneable where TFormatMetadata : class, IFormatMetadata<TFormatMetadata>
=> this.formatMetadata[key] = value; => this.formatMetadata[key] = value;
/// <inheritdoc/> /// <inheritdoc/>

4
tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs

@ -23,7 +23,7 @@ public class CurDecoderTests
Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Width, meta.EncodingWidth);
Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(image.Height, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -36,6 +36,6 @@ public class CurDecoderTests
Assert.Equal(image.Width, meta.EncodingWidth); Assert.Equal(image.Width, meta.EncodingWidth);
Assert.Equal(image.Height, meta.EncodingHeight); Assert.Equal(image.Height, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
} }

34
tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs

@ -2,9 +2,11 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Cur; using static SixLabors.ImageSharp.Tests.TestImages.Cur;
using static SixLabors.ImageSharp.Tests.TestImages.Ico;
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
@ -17,7 +19,7 @@ public class CurEncoderTests
[WithFile(CurReal, PixelTypes.Rgba32)] [WithFile(CurReal, PixelTypes.Rgba32)]
[WithFile(WindowsMouse, PixelTypes.Rgba32)] [WithFile(WindowsMouse, PixelTypes.Rgba32)]
public void CanRoundTripEncoder<TPixel>(TestImageProvider<TPixel> provider) public void CanRoundTripEncoder<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using Image<TPixel> image = provider.GetImage(CurDecoder.Instance); using Image<TPixel> image = provider.GetImage(CurDecoder.Instance);
using MemoryStream memStream = new(); using MemoryStream memStream = new();
@ -31,4 +33,34 @@ public class CurEncoderTests
encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance); encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance);
} }
[Theory]
[WithFile(Flutter, PixelTypes.Rgba32)]
public void CanConvertFromIco<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(IcoDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, Encoder);
memStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> encoded = Image.Load<TPixel>(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);
}
}
} }

16
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

@ -56,7 +56,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit1, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -92,7 +92,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit24, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -128,7 +128,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -163,7 +163,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit4, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -199,7 +199,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit8, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -229,7 +229,7 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Png, meta.Compression); Assert.Equal(IconFrameCompression.Png, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
[Theory] [Theory]
@ -262,7 +262,7 @@ public class IcoDecoderTests
{ {
ImageFrame<Rgba32> frame = image.Frames[i]; ImageFrame<Rgba32> frame = image.Frames[i];
IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata(); IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata();
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
image.DebugSaveMultiFrame(provider); image.DebugSaveMultiFrame(provider);
@ -327,6 +327,6 @@ public class IcoDecoderTests
Assert.Equal(expectedWidth, meta.EncodingWidth); Assert.Equal(expectedWidth, meta.EncodingWidth);
Assert.Equal(expectedHeight, meta.EncodingHeight); Assert.Equal(expectedHeight, meta.EncodingHeight);
Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
} }
} }

33
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs

@ -1,9 +1,11 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Cur;
using static SixLabors.ImageSharp.Tests.TestImages.Ico; using static SixLabors.ImageSharp.Tests.TestImages.Ico;
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
@ -26,9 +28,38 @@ public class IcoEncoderTests
memStream.Seek(0, SeekOrigin.Begin); memStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> encoded = Image.Load<TPixel>(memStream); using Image<TPixel> encoded = Image.Load<TPixel>(memStream);
encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false); encoded.DebugSaveMultiFrame(provider);
// Despite preservation of the palette. The process can still be lossy // Despite preservation of the palette. The process can still be lossy
encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance); encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance);
} }
[Theory]
[WithFile(WindowsMouse, PixelTypes.Rgba32)]
public void CanConvertFromCur<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(CurDecoder.Instance);
using MemoryStream memStream = new();
image.Save(memStream, Encoder);
memStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> encoded = Image.Load<TPixel>(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);
}
}
} }

Loading…
Cancel
Save