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()
=> new()
{
EncodingType = this.BitsPerPixel <= BmpBitsPerPixel.Bit8
? EncodingType.Lossy
: EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo()
};

18
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<Color>? 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;
}
}
}

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

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// IcoFrameMetadata.
/// </summary>
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
/// <summary>
/// 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/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any.
@ -69,11 +69,75 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <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/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
@ -84,7 +148,7 @@ public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, 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<CurFrameMetadata>, 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
};
}
}

173
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;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// Provides Cur specific metadata information for the image.
/// </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/>
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/>
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.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
/// <summary>
@ -9,27 +11,44 @@ namespace SixLabors.ImageSharp.Formats;
public class FormatConnectingFrameMetadata
{
/// <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>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
public TimeSpan Duration { get; init; }
/// <summary>
/// Gets or sets the frame color table mode.
/// Gets the frame alpha blending mode.
/// </summary>
public FrameColorTableMode ColorTableMode { get; set; }
public FrameBlendMode BlendMode { get; init; }
/// <summary>
/// Gets or sets the duration of the frame.
/// Gets the frame disposal mode.
/// </summary>
public TimeSpan Duration { get; set; }
public FrameDisposalMode DisposalMode { get; init; }
/// <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>
public FrameBlendMode BlendMode { get; set; }
public int? EncodingWidth { get; set; }
/// <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>
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; }
/// <summary>
/// Gets the shared color table.
/// Gets the shared color table if any.
/// </summary>
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(
ImageFrameMetadata metadata,
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
int index,
in IconDirEntry entry,
IconFrameCompression compression,
BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? 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;
}
}
}

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

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Provides Ico specific metadata information for the image frame.
/// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
/// <summary>
/// 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/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any.
@ -62,11 +62,75 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <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/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
@ -75,7 +139,7 @@ public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, 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<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.
// 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;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// </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/>
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/>
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;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> 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<Color>? 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,

2
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;

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

@ -3,7 +3,9 @@
// <auto-generated />
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);
/// <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>
/// Saves the image to the given stream with the Gif format.
/// </summary>
@ -223,6 +327,108 @@ public static partial class ImageExtensions
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.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="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>
/// Saves the image to the given stream with the Jpeg format.
/// </summary>

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

@ -4,7 +4,9 @@
// <auto-generated />
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
/// <returns>The new <see cref="BmpMetadata"/></returns>
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>
/// 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
@ -60,6 +82,26 @@ public static class ImageMetadataExtensions
/// <returns>The new <see cref="GifMetadata"/></returns>
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>
/// 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
@ -201,6 +243,46 @@ public static class ImageMetadataExtensions
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>
/// 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

4
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",

6
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -109,7 +109,9 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
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<ImageFrameMetadata>
internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable
where TFormatFrameMetadata : class, IFormatFrameMetadata<TFormatFrameMetadata>
=> this.formatMetadata[key] = value;
/// <summary>

6
src/ImageSharp/Metadata/ImageMetadata.cs

@ -194,7 +194,9 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
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<ImageMetadata>
=> ((IDeepCloneable<TFormatMetadata>)this.GetFormatMetadata(key)).DeepClone();
internal void SetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> key, TFormatMetadata value)
where TFormatMetadata : class, IDeepCloneable
where TFormatMetadata : class, IFormatMetadata<TFormatMetadata>
=> this.formatMetadata[key] = value;
/// <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.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);
}
}

34
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> 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<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(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<Rgba32> 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);
}
}

33
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<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
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