diff --git a/ImageSharp.sln b/ImageSharp.sln
index 162de84168..7ccd92c07d 100644
--- a/ImageSharp.sln
+++ b/ImageSharp.sln
@@ -661,6 +661,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Qoi", "Qoi", "{E801B508-493
tests\Images\Input\Qoi\wikipedia_008.qoi = tests\Images\Input\Qoi\wikipedia_008.qoi
EndProjectSection
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Icon", "Icon", "{95E45DDE-A67D-48AD-BBA8-5FAA151B860D}"
+ ProjectSection(SolutionItems) = preProject
+ tests\Images\Input\Icon\aero_arrow.cur = tests\Images\Input\Icon\aero_arrow.cur
+ tests\Images\Input\Icon\flutter.ico = tests\Images\Input\Icon\flutter.ico
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -714,6 +720,7 @@ Global
{670DD46C-82E9-499A-B2D2-00A802ED0141} = {E1C42A6F-913B-4A7B-B1A8-2BB62843B254}
{5DFC394F-136F-4B76-9BCA-3BA786515EFC} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
{E801B508-4935-41CD-BA85-CF11BFF55A45} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
+ {95E45DDE-A67D-48AD-BBA8-5FAA151B860D} = {9DA226A1-8656-49A8-A58A-A8B5C081AD66}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5F8B9D1F-CD8B-4CC5-8216-D531E25BD795}
diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs
index 1ca5d0a46b..d6cfd480df 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -5,6 +5,8 @@
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
+using SixLabors.ImageSharp.Formats.Icon.Cur;
+using SixLabors.ImageSharp.Formats.Icon.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
@@ -222,5 +224,7 @@ public void Configure(IImageFormatConfigurationModule configuration)
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
- new QoiConfigurationModule());
+ new QoiConfigurationModule(),
+ new IcoConfigurationModule(),
+ new CurConfigurationModule());
}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurConfigurationModule.cs b/src/ImageSharp/Formats/Icon/Cur/CurConfigurationModule.cs
new file mode 100644
index 0000000000..c975bc6099
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurConfigurationModule.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the Ico format.
+///
+public sealed class CurConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ // TODO: CurEncoder
+ // configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
+ configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
+ configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurConstants.cs b/src/ImageSharp/Formats/Icon/Cur/CurConstants.cs
new file mode 100644
index 0000000000..701b40cf4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurConstants.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Defines constants relating to ICOs
+///
+internal static class CurConstants
+{
+ ///
+ /// The list of mimetypes that equate to a ico.
+ ///
+ ///
+ /// See
+ ///
+ public static readonly IEnumerable MimeTypes = new[]
+ {
+ "application/octet-stream",
+ };
+
+ ///
+ /// The list of file extensions that equate to a ico.
+ ///
+ public static readonly IEnumerable FileExtensions = new[] { "cur" };
+
+ public const uint FileHeader = 0x00_02_00_00;
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurDecoder.cs b/src/ImageSharp/Formats/Icon/Cur/CurDecoder.cs
new file mode 100644
index 0000000000..ceefdcaba7
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurDecoder.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Decoder for generating an image out of a ico encoded stream.
+///
+public sealed class CurDecoder : ImageDecoder
+{
+ private CurDecoder()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static CurDecoder Instance { get; } = new();
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ Image image = new CurDecoderCore(options).Decode(options.Configuration, stream, cancellationToken);
+
+ ScaleToTargetSize(options, image);
+
+ return image;
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ => this.Decode(options, stream, cancellationToken);
+
+ ///
+ protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ return new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs
new file mode 100644
index 0000000000..8b08f127dc
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+internal sealed class CurDecoderCore : IconDecoderCore
+{
+ public CurDecoderCore(DecoderOptions options)
+ : base(options)
+ {
+ }
+
+ protected override IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata) => metadata.GetCurMetadata();
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurFormat.cs b/src/ImageSharp/Formats/Icon/Cur/CurFormat.cs
new file mode 100644
index 0000000000..1e5758bc4c
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurFormat.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the ICO format.
+///
+public sealed class CurFormat : IImageFormat
+{
+ private CurFormat()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static CurFormat Instance { get; } = new();
+
+ ///
+ public string Name => "ICO";
+
+ ///
+ public string DefaultMimeType => CurConstants.MimeTypes.First();
+
+ ///
+ public IEnumerable MimeTypes => CurConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => CurConstants.FileExtensions;
+
+ ///
+ public CurMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public CurFrameMetadata CreateDefaultFormatFrameMetadata() => new();
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs
new file mode 100644
index 0000000000..c94afdd3a9
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// IcoFrameMetadata
+///
+public class CurFrameMetadata : IconFrameMetadata, IDeepCloneable, IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CurFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// metadata
+ public CurFrameMetadata(IconFrameMetadata metadata)
+ : base(metadata)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// width
+ /// height
+ /// colorCount
+ /// field1
+ /// field2
+ public CurFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
+ : base(width, height, colorCount, field1, field2)
+ {
+ }
+
+ ///
+ /// Gets or sets Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
+ ///
+ public ushort HotspotX { get => this.Field1; set => this.Field1 = value; }
+
+ ///
+ /// Gets or sets Specifies the vertical coordinates of the hotspot in number of pixels from the top.
+ ///
+ public ushort HotspotY { get => this.Field2; set => this.Field2 = value; }
+
+ ///
+ public override CurFrameMetadata DeepClone() => new(this);
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Icon/Cur/CurMetadata.cs
new file mode 100644
index 0000000000..ed3c322b41
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/CurMetadata.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Provides Ico specific metadata information for the image.
+///
+public class CurMetadata : IDeepCloneable, IDeepCloneable
+{
+ ///
+ public CurMetadata DeepClone() => new();
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+}
diff --git a/src/ImageSharp/Formats/Icon/Cur/MetadataExtensions.cs b/src/ImageSharp/Formats/Icon/Cur/MetadataExtensions.cs
new file mode 100644
index 0000000000..400ece6482
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Cur/MetadataExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Cur;
+
+///
+/// Extension methods for the type.
+///
+public static class MetadataExtensions
+{
+ ///
+ /// Gets the Icon format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static CurMetadata GetCurMetadata(this ImageMetadata source)
+ => source.GetFormatMetadata(CurFormat.Instance);
+
+ ///
+ /// Gets the Icon format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source)
+ => source.GetFormatMetadata(CurFormat.Instance);
+
+ ///
+ /// Gets the Icon format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ ///
+ /// When this method returns, contains the metadata associated with the specified frame,
+ /// if found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the Icon frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(CurFormat.Instance, out metadata);
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoConfigurationModule.cs b/src/ImageSharp/Formats/Icon/Ico/IcoConfigurationModule.cs
new file mode 100644
index 0000000000..7074189c77
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoConfigurationModule.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the Ico format.
+///
+public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ // TODO: IcoEncoder
+ // configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
+ configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance);
+ configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoConstants.cs b/src/ImageSharp/Formats/Icon/Ico/IcoConstants.cs
new file mode 100644
index 0000000000..3457117056
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoConstants.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Defines constants relating to ICOs
+///
+internal static class IcoConstants
+{
+ ///
+ /// The list of mimetypes that equate to a ico.
+ ///
+ ///
+ /// See
+ ///
+ public static readonly IEnumerable MimeTypes = new[]
+ {
+ // IANA-registered
+ "image/vnd.microsoft.icon",
+
+ // ICO & CUR types used by Windows
+ "image/x-icon",
+
+ // Erroneous types but have been used
+ "image/ico",
+ "image/icon",
+ "text/ico",
+ "application/ico",
+ };
+
+ ///
+ /// The list of file extensions that equate to a ico.
+ ///
+ public static readonly IEnumerable FileExtensions = new[] { "ico" };
+
+ public const uint FileHeader = 0x00_01_00_00;
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoDecoder.cs b/src/ImageSharp/Formats/Icon/Ico/IcoDecoder.cs
new file mode 100644
index 0000000000..5d6137920f
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoDecoder.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Decoder for generating an image out of a ico encoded stream.
+///
+public sealed class IcoDecoder : ImageDecoder
+{
+ private IcoDecoder()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static IcoDecoder Instance { get; } = new();
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ Image image = new IcoDecoderCore(options).Decode(options.Configuration, stream, cancellationToken);
+
+ ScaleToTargetSize(options, image);
+
+ return image;
+ }
+
+ ///
+ protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ => this.Decode(options, stream, cancellationToken);
+
+ ///
+ protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
+ {
+ Guard.NotNull(options, nameof(options));
+ Guard.NotNull(stream, nameof(stream));
+
+ return new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs
new file mode 100644
index 0000000000..0782c21286
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+internal sealed class IcoDecoderCore : IconDecoderCore
+{
+ public IcoDecoderCore(DecoderOptions options)
+ : base(options)
+ {
+ }
+
+ protected override IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata) => metadata.GetIcoMetadata();
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoFormat.cs b/src/ImageSharp/Formats/Icon/Ico/IcoFormat.cs
new file mode 100644
index 0000000000..27b0525bfa
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoFormat.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the ICO format.
+///
+public sealed class IcoFormat : IImageFormat
+{
+ private IcoFormat()
+ {
+ }
+
+ ///
+ /// Gets the shared instance.
+ ///
+ public static IcoFormat Instance { get; } = new();
+
+ ///
+ public string Name => "ICO";
+
+ ///
+ public string DefaultMimeType => IcoConstants.MimeTypes.First();
+
+ ///
+ public IEnumerable MimeTypes => IcoConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => IcoConstants.FileExtensions;
+
+ ///
+ public IcoMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public IcoFrameMetadata CreateDefaultFormatFrameMetadata() => new();
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs
new file mode 100644
index 0000000000..7c903facd6
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// IcoFrameMetadata
+///
+public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable, IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IcoFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// metadata
+ public IcoFrameMetadata(IconFrameMetadata metadata)
+ : base(metadata)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// width
+ /// height
+ /// colorCount
+ /// field1
+ /// field2
+ public IcoFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
+ : base(width, height, colorCount, field1, field2)
+ {
+ }
+
+ ///
+ /// Gets or sets Specifies bits per pixel.
+ ///
+ ///
+ /// It may used by Encoder.
+ ///
+ public ushort BitCount { get => this.Field2; set => this.Field2 = value; }
+
+ ///
+ public override IcoFrameMetadata DeepClone() => new(this);
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Icon/Ico/IcoMetadata.cs
new file mode 100644
index 0000000000..b227d0bd6a
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/IcoMetadata.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Provides Ico specific metadata information for the image.
+///
+public class IcoMetadata : IDeepCloneable, IDeepCloneable
+{
+ ///
+ public IcoMetadata DeepClone() => new();
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+}
diff --git a/src/ImageSharp/Formats/Icon/Ico/MetadataExtensions.cs b/src/ImageSharp/Formats/Icon/Ico/MetadataExtensions.cs
new file mode 100644
index 0000000000..fb5b4afe77
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/Ico/MetadataExtensions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Icon.Ico;
+
+///
+/// Extension methods for the type.
+///
+public static class MetadataExtensions
+{
+ ///
+ /// Gets the Ico format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static IcoMetadata GetIcoMetadata(this ImageMetadata source)
+ => source.GetFormatMetadata(IcoFormat.Instance);
+
+ ///
+ /// Gets the Ico format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source)
+ => source.GetFormatMetadata(IcoFormat.Instance);
+
+ ///
+ /// Gets the Ico format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ ///
+ /// When this method returns, contains the metadata associated with the specified frame,
+ /// if found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the Ico frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(IcoFormat.Instance, out metadata);
+}
diff --git a/src/ImageSharp/Formats/Icon/IconAssert.cs b/src/ImageSharp/Formats/Icon/IconAssert.cs
new file mode 100644
index 0000000000..bcb427c1ac
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconAssert.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+internal class IconAssert
+{
+ internal static void CanSeek(Stream stream)
+ {
+ if (!stream.CanSeek)
+ {
+ throw new NotSupportedException("This stream cannot support seek");
+ }
+ }
+
+ internal static int EndOfStream(int v, int length)
+ {
+ if (v != length)
+ {
+ throw new EndOfStreamException();
+ }
+
+ return v;
+ }
+
+ internal static long EndOfStream(long v, long length)
+ {
+ if (v != length)
+ {
+ throw new EndOfStreamException();
+ }
+
+ return v;
+ }
+
+ internal static void NotSquare(in Size size)
+ {
+ if (size.Width != size.Height)
+ {
+ throw new FormatException("This image is not square.");
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs
new file mode 100644
index 0000000000..d9a578ff2d
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Metadata;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+internal abstract class IconDecoderCore : IImageDecoderInternals
+{
+ private IconDir fileHeader;
+
+ public IconDecoderCore(DecoderOptions options) => this.Options = options;
+
+ public DecoderOptions Options { get; }
+
+ public Size Dimensions { get; private set; }
+
+ protected IconDir FileHeader { get => this.fileHeader; private set => this.fileHeader = value; }
+
+ protected IconDirEntry[] Entries { get; private set; } = Array.Empty();
+
+ public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ // Stream may not at 0.
+ long basePosition = stream.Position;
+ this.ReadHeader(stream);
+
+ Span flag = stackalloc byte[Png.PngConstants.HeaderBytes.Length];
+
+ List<(Image Image, bool IsPng, int Index)> frames = new(this.Entries.Length);
+ for (int i = 0; i < this.Entries.Length; i++)
+ {
+ _ = IconAssert.EndOfStream(stream.Seek(basePosition + this.Entries[i].ImageOffset, SeekOrigin.Begin), basePosition + this.Entries[i].ImageOffset);
+ _ = IconAssert.EndOfStream(stream.Read(flag), Png.PngConstants.HeaderBytes.Length);
+ _ = stream.Seek(-Png.PngConstants.HeaderBytes.Length, SeekOrigin.Current);
+
+ bool isPng = flag.SequenceEqual(Png.PngConstants.HeaderBytes);
+
+ Image img = this.GetDecoder(isPng).Decode(stream, cancellationToken);
+ IconAssert.NotSquare(img.Size);
+ frames.Add((img, isPng, i));
+ if (isPng && img.Size.Width > this.Dimensions.Width)
+ {
+ this.Dimensions = img.Size;
+ }
+ }
+
+ ImageMetadata metadata = new();
+ return new(this.Options.Configuration, metadata, frames.Select(i =>
+ {
+ ImageFrame target = new(this.Options.Configuration, this.Dimensions);
+ ImageFrame source = i.Image.Frames.RootFrameUnsafe;
+ for (int h = 0; h < source.Height; h++)
+ {
+ source.PixelBuffer.DangerousGetRowSpan(h).CopyTo(target.PixelBuffer.DangerousGetRowSpan(h));
+ }
+
+ if (i.IsPng)
+ {
+ target.Metadata.UnsafeSetFormatMetadata(Png.PngFormat.Instance, i.Image.Metadata.GetPngMetadata());
+ }
+ else
+ {
+ target.Metadata.UnsafeSetFormatMetadata(Bmp.BmpFormat.Instance, i.Image.Metadata.GetBmpMetadata());
+ }
+
+ this.GetFrameMetadata(target.Metadata).FromIconDirEntry(this.Entries[i.Index]);
+
+ i.Image.Dispose();
+ return target;
+ }).ToArray());
+ }
+
+ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
+ {
+ this.ReadHeader(stream);
+
+ ImageMetadata metadata = new();
+ ImageFrameMetadata[] frames = new ImageFrameMetadata[this.FileHeader.Count];
+ for (int i = 0; i < frames.Length; i++)
+ {
+ frames[i] = new();
+ IconFrameMetadata icoFrameMetadata = this.GetFrameMetadata(frames[i]);
+ icoFrameMetadata.FromIconDirEntry(this.Entries[i]);
+ }
+
+ return new(new(32), new(0), metadata, frames);
+ }
+
+ protected abstract IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata);
+
+ protected void ReadHeader(Stream stream)
+ {
+ _ = Read(stream, out this.fileHeader, IconDir.Size);
+ this.Entries = new IconDirEntry[this.FileHeader.Count];
+ for (int i = 0; i < this.Entries.Length; i++)
+ {
+ _ = Read(stream, out this.Entries[i], IconDirEntry.Size);
+ }
+
+ this.Dimensions = new(
+ this.Entries.Max(i => i.Width),
+ this.Entries.Max(i => i.Height));
+ }
+
+ private static int Read(Stream stream, out T data, int size)
+ where T : unmanaged
+ {
+ Span buffer = stackalloc byte[size];
+ _ = IconAssert.EndOfStream(stream.Read(buffer), size);
+ data = MemoryMarshal.Cast(buffer)[0];
+ return size;
+ }
+
+ private IImageDecoderInternals GetDecoder(bool isPng)
+ {
+ if (isPng)
+ {
+ return new Png.PngDecoderCore(this.Options);
+ }
+ else
+ {
+ return new Bmp.BmpDecoderCore(new()
+ {
+ ProcessedAlphaMask = true,
+ SkipFileHeader = true,
+ IsDoubleHeight = true,
+ });
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs
new file mode 100644
index 0000000000..f1281a568e
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDir.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
+internal struct IconDir
+{
+ public const int Size = 3 * sizeof(ushort);
+ public ushort Reserved;
+ public IconFileType Type;
+ public ushort Count;
+
+ public IconDir(IconFileType type)
+ : this(type, 0)
+ => this.Type = type;
+
+ public IconDir(IconFileType type, ushort count)
+ {
+ this.Reserved = 0;
+ this.Type = type;
+ this.Count = count;
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
new file mode 100644
index 0000000000..43254f89d4
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
+internal struct IconDirEntry
+{
+ public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint));
+
+ public byte Width;
+
+ public byte Height;
+
+ public byte ColorCount;
+
+ public byte Reserved;
+
+ public ushort Planes;
+
+ public ushort BitCount;
+
+ public uint BytesInRes;
+
+ public uint ImageOffset;
+}
diff --git a/src/ImageSharp/Formats/Icon/IconFileType.cs b/src/ImageSharp/Formats/Icon/IconFileType.cs
new file mode 100644
index 0000000000..3450698f11
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconFileType.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// Ico file type
+///
+internal enum IconFileType : ushort
+{
+ ///
+ /// ICO file
+ ///
+ ICO = 1,
+
+ ///
+ /// CUR file
+ ///
+ CUR = 2,
+}
diff --git a/src/ImageSharp/Formats/Icon/IconFrameCompression.cs b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs
new file mode 100644
index 0000000000..f6509f40c2
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// IconFrameCompression
+///
+public enum IconFrameCompression
+{
+ ///
+ /// Unknown
+ ///
+ Unknown,
+
+ ///
+ /// Bmp
+ ///
+ Bmp,
+
+ ///
+ /// Png
+ ///
+ Png
+}
diff --git a/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs b/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs
new file mode 100644
index 0000000000..17e641c718
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconFrameMetadata.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// IconFrameMetadata
+///
+public abstract class IconFrameMetadata : IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected IconFrameMetadata()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// width
+ /// height
+ /// colorCount
+ /// field1
+ /// field2
+ protected IconFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
+ {
+ this.EncodingWidth = width;
+ this.EncodingHeight = height;
+ this.ColorCount = colorCount;
+ this.Field1 = field1;
+ this.Field2 = field2;
+ }
+
+ ///
+ protected IconFrameMetadata(IconFrameMetadata metadata)
+ {
+ this.EncodingWidth = metadata.EncodingWidth;
+ this.EncodingHeight = metadata.EncodingHeight;
+ this.ColorCount = metadata.ColorCount;
+ this.Field1 = metadata.Field1;
+ this.Field2 = metadata.Field2;
+ }
+
+ ///
+ /// Gets or sets icoFrameCompression.
+ ///
+ public IconFrameCompression Compression { get; protected set; }
+
+ ///
+ /// Gets or sets ColorCount field.
+ /// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
+ ///
+ // TODO: BmpMetadata does not supported palette yet.
+ public byte ColorCount { get; set; }
+
+ ///
+ /// Gets or sets Planes field.
+ /// In ICO format: Specifies color planes. Should be 0 or 1.
+ /// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
+ ///
+ protected ushort Field1 { get; set; }
+
+ ///
+ /// Gets or sets BitCount field.
+ /// In ICO format: Specifies bits per pixel.
+ /// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
+ ///
+ protected ushort Field2 { get; set; }
+
+ ///
+ /// Gets or sets Height field.
+ /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
+ ///
+ public byte EncodingHeight { get; set; }
+
+ ///
+ /// Gets or sets Width field.
+ /// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
+ ///
+ public byte EncodingWidth { get; set; }
+
+ ///
+ public abstract IDeepCloneable DeepClone();
+
+ internal void FromIconDirEntry(in IconDirEntry metadata)
+ {
+ this.EncodingWidth = metadata.Width;
+ this.EncodingHeight = metadata.Height;
+ this.ColorCount = metadata.ColorCount;
+ this.Field1 = metadata.Planes;
+ this.Field2 = metadata.BitCount;
+ }
+
+ internal IconDirEntry ToIconDirEntry() => new()
+ {
+ Width = this.EncodingWidth,
+ Height = this.EncodingHeight,
+ ColorCount = this.ColorCount,
+ Planes = this.Field1,
+ BitCount = this.Field2,
+ };
+}
diff --git a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
new file mode 100644
index 0000000000..aff8dfe0ac
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// Detects ico file headers.
+///
+public class IconImageFormatDetector : IImageFormatDetector
+{
+ ///
+ public int HeaderSize { get; } = 4;
+
+ ///
+ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
+ {
+ switch (MemoryMarshal.Cast(header)[0])
+ {
+ case Ico.IcoConstants.FileHeader:
+ format = Ico.IcoFormat.Instance;
+ return true;
+ case Cur.CurConstants.FileHeader:
+ format = Cur.CurFormat.Instance;
+ return true;
+ default:
+ format = default;
+ return false;
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs
index c5d61726c8..c8e6cd2657 100644
--- a/tests/ImageSharp.Tests/ConfigurationTests.cs
+++ b/tests/ImageSharp.Tests/ConfigurationTests.cs
@@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; }
- private readonly int expectedDefaultConfigurationCount = 9;
+ private readonly int expectedDefaultConfigurationCount = 11;
public ConfigurationTests()
{
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
new file mode 100644
index 0000000000..c3c9ad1c47
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon.Cur;
+using SixLabors.ImageSharp.Formats.Tiff;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.Tests.TestImages.Cur;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
+
+[Trait("Format", "Cur")]
+[ValidateDisposedMemoryAllocations]
+public class CurDecoderTests
+{
+ [Theory]
+ [WithFile(WindowsMouse, PixelTypes.Rgba32)]
+ public void CurDecoder_Decode(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(CurDecoder.Instance);
+
+ image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder());
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
new file mode 100644
index 0000000000..6e7b351113
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon.Ico;
+using SixLabors.ImageSharp.Formats.Tiff;
+using SixLabors.ImageSharp.PixelFormats;
+using static SixLabors.ImageSharp.Tests.TestImages.Ico;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
+
+[Trait("Format", "Icon")]
+[ValidateDisposedMemoryAllocations]
+public class IcoDecoderTests
+{
+ [Theory]
+ [WithFile(Flutter, PixelTypes.Rgba32)]
+ public void IcoDecoder_Decode(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder());
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index a45931e29e..c64bf2b346 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1121,4 +1121,14 @@ public static class Qoi
public const string TestCardRGBA = "Qoi/testcard_rgba.qoi";
public const string Wikipedia008 = "Qoi/wikipedia_008.qoi";
}
+
+ public static class Ico
+ {
+ public const string Flutter = "Icon/flutter.ico";
+ }
+
+ public static class Cur
+ {
+ public const string WindowsMouse = "Icon/aero_arrow.cur";
+ }
}
diff --git a/tests/Images/Input/Icon/aero_arrow.cur b/tests/Images/Input/Icon/aero_arrow.cur
new file mode 100644
index 0000000000..82cbbd33e6
--- /dev/null
+++ b/tests/Images/Input/Icon/aero_arrow.cur
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:06678bbf954f0bece61062633dc63a52a34a6f3c27ac7108f28c0f0d26bb22a7
+size 136606
diff --git a/tests/Images/Input/Icon/flutter.ico b/tests/Images/Input/Icon/flutter.ico
new file mode 100644
index 0000000000..4001f14268
--- /dev/null
+++ b/tests/Images/Input/Icon/flutter.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c098d3fc85cacff98b8e69811b48e9f0d852fcee278132d794411d978869cbf8
+size 33772