diff --git a/ImageSharp.sln b/ImageSharp.sln
index 162de8416..7ccd92c07 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 1ca5d0a46..1d9f3bb85 100644
--- a/src/ImageSharp/Configuration.cs
+++ b/src/ImageSharp/Configuration.cs
@@ -4,7 +4,9 @@
using System.Collections.Concurrent;
using SixLabors.ImageSharp.Formats;
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;
@@ -222,5 +224,7 @@ public sealed class Configuration
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
- new QoiConfigurationModule());
+ new QoiConfigurationModule(),
+ new IcoConfigurationModule(),
+ new CurConfigurationModule());
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpConstants.cs b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
index 5cf0c9732..62edfdfdf 100644
--- a/src/ImageSharp/Formats/Bmp/BmpConstants.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpConstants.cs
@@ -11,7 +11,12 @@ internal static class BmpConstants
///
/// The list of mimetypes that equate to a bmp.
///
- public static readonly IEnumerable MimeTypes = new[] { "image/bmp", "image/x-windows-bmp" };
+ public static readonly IEnumerable MimeTypes = new[]
+ {
+ "image/bmp",
+ "image/x-windows-bmp",
+ "image/x-win-bitmap"
+ };
///
/// The list of file extensions that equate to a bmp.
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index bed489752..c26536fd1 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@@ -71,7 +72,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
///
/// The file header containing general information.
///
- private BmpFileHeader fileHeader;
+ private BmpFileHeader? fileHeader;
///
/// Indicates which bitmap file marker was read.
@@ -99,6 +100,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
///
private readonly RleSkippedPixelHandling rleSkippedPixelHandling;
+ ///
+ private readonly bool processedAlphaMask;
+
+ ///
+ private readonly bool skipFileHeader;
+
+ ///
+ private readonly bool isDoubleHeight;
+
///
/// Initializes a new instance of the class.
///
@@ -109,6 +119,9 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
this.rleSkippedPixelHandling = options.RleSkippedPixelHandling;
this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
+ this.processedAlphaMask = options.ProcessedAlphaMask;
+ this.skipFileHeader = options.SkipFileHeader;
+ this.isDoubleHeight = options.UseDoubleHeight;
}
///
@@ -132,38 +145,44 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
switch (this.infoHeader.Compression)
{
- case BmpCompression.RGB:
- if (this.infoHeader.BitsPerPixel == 32)
- {
- if (this.bmpMetadata.InfoHeaderType == BmpInfoHeaderType.WinVersion3)
- {
- this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
- }
- else
- {
- this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
- }
- }
- else if (this.infoHeader.BitsPerPixel == 24)
- {
- this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
- }
- else if (this.infoHeader.BitsPerPixel == 16)
- {
- this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
- }
- else if (this.infoHeader.BitsPerPixel <= 8)
- {
- this.ReadRgbPalette(
- stream,
- pixels,
- palette,
- this.infoHeader.Width,
- this.infoHeader.Height,
- this.infoHeader.BitsPerPixel,
- bytesPerColorMapEntry,
- inverted);
- }
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3:
+ this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+
+ break;
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32:
+ this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+
+ break;
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24:
+ this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+
+ break;
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16:
+ this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
+
+ break;
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask:
+ this.ReadRgbPaletteWithAlphaMask(
+ stream,
+ pixels,
+ palette,
+ this.infoHeader.Width,
+ this.infoHeader.Height,
+ this.infoHeader.BitsPerPixel,
+ bytesPerColorMapEntry,
+ inverted);
+
+ break;
+ case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8:
+ this.ReadRgbPalette(
+ stream,
+ pixels,
+ palette,
+ this.infoHeader.Width,
+ this.infoHeader.Height,
+ this.infoHeader.BitsPerPixel,
+ bytesPerColorMapEntry,
+ inverted);
break;
@@ -839,6 +858,108 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
}
}
+ ///
+ private void ReadRgbPaletteWithAlphaMask(BufferedReadStream stream, Buffer2D pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
+ where TPixel : unmanaged, IPixel
+ {
+ // Pixels per byte (bits per pixel).
+ int ppb = 8 / bitsPerPixel;
+
+ int arrayWidth = (width + ppb - 1) / ppb;
+
+ // Bit mask
+ int mask = 0xFF >> (8 - bitsPerPixel);
+
+ // Rows are aligned on 4 byte boundaries.
+ int padding = arrayWidth % 4;
+ if (padding != 0)
+ {
+ padding = 4 - padding;
+ }
+
+ Bgra32[,] image = new Bgra32[height, width];
+ using (IMemoryOwner row = this.memoryAllocator.Allocate(arrayWidth + padding, AllocationOptions.Clean))
+ {
+ Span rowSpan = row.GetSpan();
+
+ for (int y = 0; y < height; y++)
+ {
+ int newY = Invert(y, height, inverted);
+ if (stream.Read(rowSpan) == 0)
+ {
+ BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
+ }
+
+ int offset = 0;
+
+ for (int x = 0; x < arrayWidth; x++)
+ {
+ int colOffset = x * ppb;
+ for (int shift = 0, newX = colOffset; shift < ppb && newX < width; shift++, newX++)
+ {
+ int colorIndex = ((rowSpan[offset] >> (8 - bitsPerPixel - (shift * bitsPerPixel))) & mask) * bytesPerColorMapEntry;
+
+ image[newY, newX] = Bgra32.FromBgr24(Unsafe.As(ref colors[colorIndex]));
+ }
+
+ offset++;
+ }
+ }
+ }
+
+ arrayWidth = width / 8;
+ padding = arrayWidth % 4;
+ if (padding != 0)
+ {
+ padding = 4 - padding;
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ int newY = Invert(y, height, inverted);
+
+ for (int i = 0; i < arrayWidth; i++)
+ {
+ int x = i * 8;
+ int and = stream.ReadByte();
+ if (and is -1)
+ {
+ throw new EndOfStreamException();
+ }
+
+ for (int j = 0; j < 8; j++)
+ {
+ SetAlpha(ref image[newY, x + j], and, j);
+ }
+ }
+
+ stream.Skip(padding);
+ }
+
+ for (int y = 0; y < height; y++)
+ {
+ int newY = Invert(y, height, inverted);
+ Span pixelRow = pixels.DangerousGetRowSpan(newY);
+
+ for (int x = 0; x < width; x++)
+ {
+ pixelRow[x] = TPixel.FromBgra32(image[newY, x]);
+ }
+ }
+ }
+
+ ///
+ /// Set pixel's alpha with alpha mask.
+ ///
+ /// Bgra32 pixel.
+ /// alpha mask.
+ /// bit index of pixel.
+ private static void SetAlpha(ref Bgra32 pixel, in int mask, in int index)
+ {
+ bool isTransparently = (mask & (0b10000000 >> index)) is not 0;
+ pixel.A = isTransparently ? byte.MinValue : byte.MaxValue;
+ }
+
///
/// Reads the 16 bit color palette from the stream.
///
@@ -1333,6 +1454,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
this.metadata.VerticalResolution = Math.Round(UnitConverter.InchToMeter(ImageMetadata.DefaultVerticalResolution));
}
+ if (this.isDoubleHeight)
+ {
+ this.infoHeader.Height >>= 1;
+ }
+
ushort bitsPerPixel = this.infoHeader.BitsPerPixel;
this.bmpMetadata = this.metadata.GetBmpMetadata();
this.bmpMetadata.InfoHeaderType = infoHeaderType;
@@ -1362,9 +1488,9 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// The bitmap file header of the first image follows the array header.
stream.Read(buffer, 0, BmpFileHeader.Size);
this.fileHeader = BmpFileHeader.Parse(buffer);
- if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
+ if (this.fileHeader.Value.Type != BmpConstants.TypeMarkers.Bitmap)
{
- BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Type}'.");
+ BmpThrowHelper.ThrowNotSupportedException($"Unsupported bitmap file inside a BitmapArray file. File header bitmap type marker '{this.fileHeader.Value.Type}'.");
}
break;
@@ -1387,7 +1513,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
[MemberNotNull(nameof(bmpMetadata))]
private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette)
{
- this.ReadFileHeader(stream);
+ if (!this.skipFileHeader)
+ {
+ this.ReadFileHeader(stream);
+ }
+
this.ReadInfoHeader(stream);
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
@@ -1411,7 +1541,21 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
switch (this.fileMarkerType)
{
case BmpFileMarkerType.Bitmap:
- colorMapSizeBytes = this.fileHeader.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
+ if (this.fileHeader.HasValue)
+ {
+ colorMapSizeBytes = this.fileHeader.Value.Offset - BmpFileHeader.Size - this.infoHeader.HeaderSize;
+ }
+ else
+ {
+ colorMapSizeBytes = this.infoHeader.ClrUsed;
+ if (colorMapSizeBytes is 0 && this.infoHeader.BitsPerPixel is <= 8)
+ {
+ colorMapSizeBytes = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
+ }
+
+ colorMapSizeBytes *= 4;
+ }
+
int colorCountForBitDepth = ColorNumerics.GetColorCountForBitDepth(this.infoHeader.BitsPerPixel);
bytesPerColorMapEntry = colorMapSizeBytes / colorCountForBitDepth;
@@ -1442,7 +1586,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
{
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit.
// Make sure, that we will not read pass the bitmap offset (starting position of image data).
- if (stream.Position > this.fileHeader.Offset - colorMapSizeBytes)
+ if (this.fileHeader.HasValue && stream.Position > this.fileHeader.Value.Offset - colorMapSizeBytes)
{
BmpThrowHelper.ThrowInvalidImageContentException(
$"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");
@@ -1456,7 +1600,20 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
}
}
- int skipAmount = this.fileHeader.Offset - (int)stream.Position;
+ if (palette.Length > 0)
+ {
+ Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()];
+ ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette);
+ Color.FromPixel(rgbTable, colorTable);
+ this.bmpMetadata.ColorTable = colorTable;
+ }
+
+ int skipAmount = 0;
+ if (this.fileHeader.HasValue)
+ {
+ skipAmount = this.fileHeader.Value.Offset - (int)stream.Position;
+ }
+
if ((skipAmount + (int)stream.Position) > stream.Length)
{
BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length.");
diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs
index b3387ce80..158a9d479 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs
@@ -16,4 +16,28 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions
/// which can occur during decoding run length encoded bitmaps.
///
public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; }
+
+ ///
+ /// Gets a value indicating whether the additional alpha mask is processed at decoding time.
+ ///
+ ///
+ /// Used by the icon decoder.
+ ///
+ internal bool ProcessedAlphaMask { get; init; }
+
+ ///
+ /// Gets a value indicating whether to skip loading the BMP file header.
+ ///
+ ///
+ /// Used by the icon decoder.
+ ///
+ internal bool SkipFileHeader { get; init; }
+
+ ///
+ /// Gets a value indicating whether to treat the height as double of true height.
+ ///
+ ///
+ /// Used by the icon decoder.
+ ///
+ internal bool UseDoubleHeight { get; init; }
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
index 0081f6a1a..0be243f9a 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs
@@ -29,6 +29,15 @@ public sealed class BmpEncoder : QuantizingImageEncoder
///
public bool SupportTransparency { get; init; }
+ ///
+ internal bool ProcessedAlphaMask { get; init; }
+
+ ///
+ internal bool SkipFileHeader { get; init; }
+
+ ///
+ internal bool UseDoubleHeight { get; init; }
+
///
protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
{
diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
index 076d1adf0..151da1828 100644
--- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
@@ -4,7 +4,6 @@
using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
-using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@@ -92,6 +91,15 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
///
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
+ ///
+ private readonly bool processedAlphaMask;
+
+ ///
+ private readonly bool skipFileHeader;
+
+ ///
+ private readonly bool isDoubleHeight;
+
///
/// Initializes a new instance of the class.
///
@@ -101,9 +109,14 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{
this.memoryAllocator = memoryAllocator;
this.bitsPerPixel = encoder.BitsPerPixel;
+
+ // TODO: Use a palette quantizer if supplied.
this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3;
+ this.processedAlphaMask = encoder.ProcessedAlphaMask;
+ this.skipFileHeader = encoder.SkipFileHeader;
+ this.isDoubleHeight = encoder.UseDoubleHeight;
}
///
@@ -119,6 +132,9 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
+ // Stream may not at 0.
+ long basePosition = stream.Position;
+
Configuration configuration = image.Configuration;
ImageMetadata metadata = image.Metadata;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
@@ -154,14 +170,26 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
_ => BmpInfoHeader.SizeV3
};
- BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, image.Height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
+ // for ico/cur encoder.
+ int height = image.Height;
+ if (this.isDoubleHeight)
+ {
+ height <<= 1;
+ }
+
+ BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span buffer = stackalloc byte[infoHeaderSize];
- WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
+ // for ico/cur encoder.
+ if (!this.skipFileHeader)
+ {
+ WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
+ }
+
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image);
- WriteColorProfile(stream, iccProfileData, buffer);
+ WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
}
@@ -245,16 +273,20 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// The stream to write to.
/// The color profile data.
/// The buffer.
- private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span buffer)
+ /// The Stream may not be start with 0.
+ private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span buffer, long basePosition)
{
if (iccProfileData != null)
{
// The offset, in bytes, from the beginning of the BITMAPV5HEADER structure to the start of the profile data.
int streamPositionAfterImageData = (int)stream.Position - BmpFileHeader.Size;
stream.Write(iccProfileData);
+ long position = stream.Position; // Storage Position
BinaryPrimitives.WriteInt32LittleEndian(buffer, streamPositionAfterImageData);
- stream.Position = BmpFileHeader.Size + 112;
+ _ = stream.Seek(basePosition, SeekOrigin.Begin);
+ _ = stream.Seek(BmpFileHeader.Size + 112, SeekOrigin.Current);
stream.Write(buffer[..4]);
+ _ = stream.Seek(position, SeekOrigin.Begin); // Reset Position
}
}
@@ -347,6 +379,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
this.Write1BitPixelData(configuration, stream, image);
break;
}
+
+ if (this.processedAlphaMask)
+ {
+ ProcessedAlphaMask(stream, image);
+ }
}
private IMemoryOwner AllocateRow(int width, int bytesPerPixel)
@@ -722,4 +759,45 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(indices);
}
+
+ private static void ProcessedAlphaMask(Stream stream, Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ int arrayWidth = image.Width / 8;
+ int padding = arrayWidth % 4;
+ if (padding is not 0)
+ {
+ padding = 4 - padding;
+ }
+
+ Span mask = stackalloc byte[arrayWidth];
+ for (int y = image.Height - 1; y >= 0; y--)
+ {
+ mask.Clear();
+ Span row = image.GetRootFramePixelBuffer().DangerousGetRowSpan(y);
+
+ for (int i = 0; i < arrayWidth; i++)
+ {
+ int x = i * 8;
+
+ for (int j = 0; j < 8; j++)
+ {
+ WriteAlphaMask(row[x + j], ref mask[i], j);
+ }
+ }
+
+ stream.Write(mask);
+ stream.Skip(padding);
+ }
+ }
+
+ private static void WriteAlphaMask(in TPixel pixel, ref byte mask, in int index)
+ where TPixel : unmanaged, IPixel
+ {
+ Rgba32 rgba = pixel.ToRgba32();
+ if (rgba.A is 0)
+ {
+ mask |= unchecked((byte)(0b10000000 >> index));
+ }
+ }
}
diff --git a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
index a2ed1d21d..a50023b27 100644
--- a/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpMetadata.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Bmp;
@@ -23,6 +23,11 @@ public class BmpMetadata : IDeepCloneable
{
this.BitsPerPixel = other.BitsPerPixel;
this.InfoHeaderType = other.InfoHeaderType;
+
+ if (other.ColorTable?.Length > 0)
+ {
+ this.ColorTable = other.ColorTable.Value.ToArray();
+ }
}
///
@@ -35,8 +40,11 @@ public class BmpMetadata : IDeepCloneable
///
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
+ ///
+ /// Gets or sets the color table, if any.
+ ///
+ public ReadOnlyMemory? ColorTable { get; set; }
+
///
public IDeepCloneable DeepClone() => new BmpMetadata(this);
-
- // TODO: Colors used once we support encoding palette bmps.
}
diff --git a/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
new file mode 100644
index 000000000..879b3f112
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the Ico format.
+///
+public sealed class CurConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ 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/Cur/CurConstants.cs b/src/ImageSharp/Formats/Cur/CurConstants.cs
new file mode 100644
index 000000000..7abf4c812
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurConstants.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+///
+/// Defines constants relating to ICOs
+///
+internal static class CurConstants
+{
+ ///
+ /// The list of mime types that equate to a cur.
+ ///
+ ///
+ /// See
+ ///
+ public static readonly IEnumerable MimeTypes =
+ [
+
+ // 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 cur.
+ ///
+ public static readonly IEnumerable FileExtensions = ["cur"];
+
+ public const uint FileHeader = 0x00_02_00_00;
+}
diff --git a/src/ImageSharp/Formats/Cur/CurDecoder.cs b/src/ImageSharp/Formats/Cur/CurDecoder.cs
new file mode 100644
index 000000000..cbe646c47
--- /dev/null
+++ b/src/ImageSharp/Formats/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.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/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
new file mode 100644
index 000000000..3018ec6bf
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Icon;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+internal sealed class CurDecoderCore : IconDecoderCore
+{
+ public CurDecoderCore(DecoderOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void SetFrameMetadata(
+ ImageFrameMetadata metadata,
+ in IconDirEntry entry,
+ IconFrameCompression compression,
+ BmpBitsPerPixel bitsPerPixel,
+ ReadOnlyMemory? colorTable)
+ {
+ CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata();
+ curFrameMetadata.FromIconDirEntry(entry);
+ curFrameMetadata.Compression = compression;
+ curFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
+ curFrameMetadata.ColorTable = colorTable;
+ }
+}
diff --git a/src/ImageSharp/Formats/Cur/CurEncoder.cs b/src/ImageSharp/Formats/Cur/CurEncoder.cs
new file mode 100644
index 000000000..e19a73990
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurEncoder.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+///
+/// Image encoder for writing an image to a stream as a Windows Cursor.
+///
+public sealed class CurEncoder : QuantizingImageEncoder
+{
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ CurEncoderCore encoderCore = new(this);
+ encoderCore.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs
new file mode 100644
index 000000000..6435587e2
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Cur;
+
+internal sealed class CurEncoderCore : IconEncoderCore
+{
+ public CurEncoderCore(QuantizingImageEncoder encoder)
+ : base(encoder, IconFileType.CUR)
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Cur/CurFormat.cs b/src/ImageSharp/Formats/Cur/CurFormat.cs
new file mode 100644
index 000000000..af93382ec
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurFormat.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.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/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
new file mode 100644
index 000000000..014944ba6
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
@@ -0,0 +1,100 @@
+// 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;
+
+///
+/// IcoFrameMetadata.
+///
+public class CurFrameMetadata : IDeepCloneable, IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CurFrameMetadata()
+ {
+ }
+
+ private CurFrameMetadata(CurFrameMetadata 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;
+ }
+
+ ///
+ /// Gets or sets the frame compressions format.
+ ///
+ public IconFrameCompression Compression { get; set; }
+
+ ///
+ /// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left.
+ ///
+ public ushort HotspotX { get; set; }
+
+ ///
+ /// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top.
+ ///
+ public ushort HotspotY { get; set; }
+
+ ///
+ /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
+ ///
+ public byte EncodingWidth { get; set; }
+
+ ///
+ /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
+ ///
+ public byte EncodingHeight { get; set; }
+
+ ///
+ /// Gets or sets the number of bits per pixel.
+ /// Used when is
+ ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
+
+ ///
+ /// Gets or sets the color table, if any.
+ /// The underlying pixel format is represented by .
+ ///
+ public ReadOnlyMemory? ColorTable { get; set; }
+
+ ///
+ public CurFrameMetadata DeepClone() => new(this);
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+
+ internal void FromIconDirEntry(IconDirEntry entry)
+ {
+ this.EncodingWidth = entry.Width;
+ this.EncodingHeight = entry.Height;
+ this.HotspotX = entry.Planes;
+ this.HotspotY = entry.BitCount;
+ }
+
+ internal IconDirEntry ToIconDirEntry()
+ {
+ byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8
+ ? (byte)0
+ : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
+
+ return new()
+ {
+ Width = this.EncodingWidth,
+ Height = this.EncodingHeight,
+ Planes = this.HotspotX,
+ BitCount = this.HotspotY,
+ ColorCount = colorCount
+ };
+ }
+}
diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs
new file mode 100644
index 000000000..5c3486d4a
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.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/Cur/MetadataExtensions.cs b/src/ImageSharp/Formats/Cur/MetadataExtensions.cs
new file mode 100644
index 000000000..6394c564b
--- /dev/null
+++ b/src/ImageSharp/Formats/Cur/MetadataExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp;
+
+///
+/// Extension methods for the type.
+///
+public static partial class MetadataExtensions
+{
+ ///
+ /// Gets the Icon format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static CurMetadata GetCurMetadata(this ImageMetadata source)
+ => source.GetFormatMetadata(CurFormat.Instance);
+
+ ///
+ /// Gets the Icon format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source)
+ => source.GetFormatMetadata(CurFormat.Instance);
+
+ ///
+ /// Gets the Icon format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ ///
+ /// When this method returns, contains the metadata associated with the specified frame,
+ /// if found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the Icon frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetCurMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out CurFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(CurFormat.Instance, out metadata);
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
new file mode 100644
index 000000000..224aaa31e
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+///
+/// Registers the image encoders, decoders and mime type detectors for the Ico format.
+///
+public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
+{
+ ///
+ public void Configure(Configuration configuration)
+ {
+ 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/Ico/IcoConstants.cs b/src/ImageSharp/Formats/Ico/IcoConstants.cs
new file mode 100644
index 000000000..116579368
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoConstants.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+///
+/// Defines constants relating to ICOs
+///
+internal static class IcoConstants
+{
+ ///
+ /// The list of mime types that equate to a ico.
+ ///
+ ///
+ /// See
+ ///
+ public static readonly IEnumerable MimeTypes =
+ [
+
+ // 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 = ["ico"];
+
+ public const uint FileHeader = 0x00_01_00_00;
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoDecoder.cs b/src/ImageSharp/Formats/Ico/IcoDecoder.cs
new file mode 100644
index 000000000..a0c8a3075
--- /dev/null
+++ b/src/ImageSharp/Formats/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.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/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
new file mode 100644
index 000000000..f4990c66a
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Icon;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+internal sealed class IcoDecoderCore : IconDecoderCore
+{
+ public IcoDecoderCore(DecoderOptions options)
+ : base(options)
+ {
+ }
+
+ protected override void SetFrameMetadata(
+ ImageFrameMetadata metadata,
+ in IconDirEntry entry,
+ IconFrameCompression compression,
+ BmpBitsPerPixel bitsPerPixel,
+ ReadOnlyMemory? colorTable)
+ {
+ IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata();
+ icoFrameMetadata.FromIconDirEntry(entry);
+ icoFrameMetadata.Compression = compression;
+ icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
+ icoFrameMetadata.ColorTable = colorTable;
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoEncoder.cs b/src/ImageSharp/Formats/Ico/IcoEncoder.cs
new file mode 100644
index 000000000..e72925156
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoEncoder.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+///
+/// Image encoder for writing an image to a stream as a Windows Icon.
+///
+public sealed class IcoEncoder : QuantizingImageEncoder
+{
+ ///
+ protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken)
+ {
+ IcoEncoderCore encoderCore = new(this);
+ encoderCore.Encode(image, stream, cancellationToken);
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
new file mode 100644
index 000000000..f3cacb1b9
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Icon;
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+internal sealed class IcoEncoderCore : IconEncoderCore
+{
+ public IcoEncoderCore(QuantizingImageEncoder encoder)
+ : base(encoder, IconFileType.ICO)
+ {
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoFormat.cs b/src/ImageSharp/Formats/Ico/IcoFormat.cs
new file mode 100644
index 000000000..5f89ab7f2
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoFormat.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.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/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
new file mode 100644
index 000000000..ea27d13c8
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
@@ -0,0 +1,95 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Icon;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Ico;
+
+///
+/// Provides Ico specific metadata information for the image frame.
+///
+public class IcoFrameMetadata : IDeepCloneable, IDeepCloneable
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IcoFrameMetadata()
+ {
+ }
+
+ private IcoFrameMetadata(IcoFrameMetadata other)
+ {
+ this.Compression = other.Compression;
+ this.EncodingWidth = other.EncodingWidth;
+ this.EncodingHeight = other.EncodingHeight;
+ this.BmpBitsPerPixel = other.BmpBitsPerPixel;
+
+ if (other.ColorTable?.Length > 0)
+ {
+ this.ColorTable = other.ColorTable.Value.ToArray();
+ }
+ }
+
+ ///
+ /// Gets or sets the frame compressions format.
+ ///
+ public IconFrameCompression Compression { get; set; }
+
+ ///
+ /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
+ ///
+ public byte EncodingWidth { get; set; }
+
+ ///
+ /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
+ ///
+ public byte EncodingHeight { get; set; }
+
+ ///
+ /// Gets or sets the number of bits per pixel.
+ /// Used when is
+ ///
+ public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
+
+ ///
+ /// Gets or sets the color table, if any.
+ /// The underlying pixel format is represented by .
+ ///
+ public ReadOnlyMemory? ColorTable { get; set; }
+
+ ///
+ public IcoFrameMetadata DeepClone() => new(this);
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+
+ internal void FromIconDirEntry(IconDirEntry entry)
+ {
+ this.EncodingWidth = entry.Width;
+ this.EncodingHeight = entry.Height;
+ }
+
+ internal IconDirEntry ToIconDirEntry()
+ {
+ byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Pixel8
+ ? (byte)0
+ : (byte)ColorNumerics.GetColorCountForBitDepth((int)this.BmpBitsPerPixel);
+
+ return new()
+ {
+ Width = this.EncodingWidth,
+ Height = this.EncodingHeight,
+ Planes = 1,
+ ColorCount = colorCount,
+ BitCount = this.Compression switch
+ {
+ IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel,
+ IconFrameCompression.Png or _ => 32,
+ },
+ };
+ }
+}
diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
new file mode 100644
index 000000000..f165bf916
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.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/Ico/MetadataExtensions.cs b/src/ImageSharp/Formats/Ico/MetadataExtensions.cs
new file mode 100644
index 000000000..497375f99
--- /dev/null
+++ b/src/ImageSharp/Formats/Ico/MetadataExtensions.cs
@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Ico;
+using SixLabors.ImageSharp.Metadata;
+
+namespace SixLabors.ImageSharp;
+
+///
+/// Extension methods for the type.
+///
+public static partial class MetadataExtensions
+{
+ ///
+ /// Gets the Ico format specific metadata for the image.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static IcoMetadata GetIcoMetadata(this ImageMetadata source)
+ => source.GetFormatMetadata(IcoFormat.Instance);
+
+ ///
+ /// Gets the Ico format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ /// The .
+ public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source)
+ => source.GetFormatMetadata(IcoFormat.Instance);
+
+ ///
+ /// Gets the Ico format specific metadata for the image frame.
+ ///
+ /// The metadata this method extends.
+ ///
+ /// When this method returns, contains the metadata associated with the specified frame,
+ /// if found; otherwise, the default value for the type of the metadata parameter.
+ /// This parameter is passed uninitialized.
+ ///
+ ///
+ /// if the Ico frame metadata exists; otherwise, .
+ ///
+ public static bool TryGetIcoMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out IcoFrameMetadata? metadata)
+ => source.TryGetFormatMetadata(IcoFormat.Instance, out metadata);
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs
new file mode 100644
index 000000000..74fe7b9e5
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs
@@ -0,0 +1,306 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Png;
+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;
+ private IconDirEntry[]? entries;
+
+ protected IconDecoderCore(DecoderOptions options)
+ => this.Options = options;
+
+ public DecoderOptions Options { get; }
+
+ public Size Dimensions { get; private set; }
+
+ 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[PngConstants.HeaderBytes.Length];
+
+ List<(Image Image, IconFrameCompression Compression, int Index)> decodedEntries
+ = new((int)Math.Min(this.entries.Length, this.Options.MaxFrames));
+
+ for (int i = 0; i < this.entries.Length; i++)
+ {
+ if (i == this.Options.MaxFrames)
+ {
+ break;
+ }
+
+ ref IconDirEntry entry = ref this.entries[i];
+
+ // If we hit the end of the stream we should break.
+ if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length)
+ {
+ break;
+ }
+
+ // There should always be enough bytes for this regardless of the entry type.
+ if (stream.Read(flag) != PngConstants.HeaderBytes.Length)
+ {
+ break;
+ }
+
+ // Reset the stream position.
+ _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current);
+
+ bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
+
+ // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
+ Image temp = this.GetDecoder(isPng).Decode(stream, cancellationToken);
+ decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i));
+
+ // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
+ // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
+ this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height));
+ }
+
+ ImageMetadata metadata = new();
+ BmpMetadata? bmpMetadata = null;
+ PngMetadata? pngMetadata = null;
+ Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
+ {
+ BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
+ ReadOnlyMemory? colorTable = null;
+ ImageFrame target = new(this.Options.Configuration, this.Dimensions);
+ ImageFrame source = x.Image.Frames.RootFrameUnsafe;
+ for (int y = 0; y < source.Height; y++)
+ {
+ source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y));
+ }
+
+ // Copy the format specific frame metadata to the image.
+ if (x.Compression is IconFrameCompression.Png)
+ {
+ if (x.Index == 0)
+ {
+ pngMetadata = x.Image.Metadata.GetPngMetadata();
+ }
+
+ target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngMetadata());
+ }
+ else
+ {
+ BmpMetadata meta = x.Image.Metadata.GetBmpMetadata();
+ bitsPerPixel = meta.BitsPerPixel;
+ colorTable = meta.ColorTable;
+
+ if (x.Index == 0)
+ {
+ bmpMetadata = meta;
+ }
+ }
+
+ this.SetFrameMetadata(
+ target.Metadata,
+ this.entries[x.Index],
+ x.Compression,
+ bitsPerPixel,
+ colorTable);
+
+ x.Image.Dispose();
+
+ return target;
+ }).ToArray());
+
+ // Copy the format specific metadata to the image.
+ if (bmpMetadata != null)
+ {
+ result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
+ }
+
+ if (pngMetadata != null)
+ {
+ result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
+ }
+
+ return result;
+ }
+
+ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
+ {
+ // Stream may not at 0.
+ long basePosition = stream.Position;
+ this.ReadHeader(stream);
+
+ Span flag = stackalloc byte[PngConstants.HeaderBytes.Length];
+
+ ImageMetadata metadata = new();
+ BmpMetadata? bmpMetadata = null;
+ PngMetadata? pngMetadata = null;
+ ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)];
+ int bpp = 0;
+ for (int i = 0; i < frames.Length; i++)
+ {
+ BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
+ ReadOnlyMemory? colorTable = null;
+ ref IconDirEntry entry = ref this.entries[i];
+
+ // If we hit the end of the stream we should break.
+ if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length)
+ {
+ break;
+ }
+
+ // There should always be enough bytes for this regardless of the entry type.
+ if (stream.Read(flag) != PngConstants.HeaderBytes.Length)
+ {
+ break;
+ }
+
+ // Reset the stream position.
+ _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current);
+
+ bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
+
+ // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
+ ImageInfo temp = this.GetDecoder(isPng).Identify(stream, cancellationToken);
+
+ ImageFrameMetadata frameMetadata = new();
+
+ if (isPng)
+ {
+ if (i == 0)
+ {
+ pngMetadata = temp.Metadata.GetPngMetadata();
+ }
+
+ frameMetadata.SetFormatMetadata(PngFormat.Instance, temp.FrameMetadataCollection[0].GetPngMetadata());
+ }
+ else
+ {
+ BmpMetadata meta = temp.Metadata.GetBmpMetadata();
+ bitsPerPixel = meta.BitsPerPixel;
+ colorTable = meta.ColorTable;
+
+ if (i == 0)
+ {
+ bmpMetadata = meta;
+ }
+ }
+
+ bpp = Math.Max(bpp, (int)bitsPerPixel);
+
+ frames[i] = frameMetadata;
+
+ this.SetFrameMetadata(
+ frames[i],
+ this.entries[i],
+ isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp,
+ bitsPerPixel,
+ colorTable);
+
+ // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
+ // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
+ this.Dimensions = new(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height));
+ }
+
+ // Copy the format specific metadata to the image.
+ if (bmpMetadata != null)
+ {
+ metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
+ }
+
+ if (pngMetadata != null)
+ {
+ metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
+ }
+
+ return new(new(bpp), this.Dimensions, metadata, frames);
+ }
+
+ protected abstract void SetFrameMetadata(
+ ImageFrameMetadata metadata,
+ in IconDirEntry entry,
+ IconFrameCompression compression,
+ BmpBitsPerPixel bitsPerPixel,
+ ReadOnlyMemory? colorTable);
+
+ [MemberNotNull(nameof(entries))]
+ protected void ReadHeader(Stream stream)
+ {
+ Span buffer = stackalloc byte[IconDirEntry.Size];
+
+ // ICONDIR
+ _ = CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size);
+ this.fileHeader = IconDir.Parse(buffer);
+
+ // ICONDIRENTRY
+ this.entries = new IconDirEntry[this.fileHeader.Count];
+ for (int i = 0; i < this.entries.Length; i++)
+ {
+ _ = CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size);
+ this.entries[i] = IconDirEntry.Parse(buffer);
+ }
+
+ int width = 0;
+ int height = 0;
+ foreach (IconDirEntry entry in this.entries)
+ {
+ // Since Windows 95 size of an image in the ICONDIRENTRY structure might
+ // be set to zero, which means 256 pixels.
+ if (entry.Width == 0)
+ {
+ width = 256;
+ }
+
+ if (entry.Height == 0)
+ {
+ height = 256;
+ }
+
+ if (width == 256 && height == 256)
+ {
+ break;
+ }
+
+ width = Math.Max(width, entry.Width);
+ height = Math.Max(height, entry.Height);
+ }
+
+ this.Dimensions = new(width, height);
+ }
+
+ private IImageDecoderInternals GetDecoder(bool isPng)
+ {
+ if (isPng)
+ {
+ return new PngDecoderCore(new()
+ {
+ GeneralOptions = this.Options,
+ });
+ }
+
+ return new BmpDecoderCore(new()
+ {
+ GeneralOptions = this.Options,
+ ProcessedAlphaMask = true,
+ SkipFileHeader = true,
+ UseDoubleHeight = true,
+ });
+ }
+
+ private static int CheckEndOfStream(int v, int length)
+ {
+ if (v != length)
+ {
+ throw new InvalidImageContentException("Not enough bytes to read icon header.");
+ }
+
+ return v;
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs
new file mode 100644
index 000000000..3e02538c8
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDir.cs
@@ -0,0 +1,43 @@
+// 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(ushort reserved, IconFileType type, ushort count)
+{
+ public const int Size = 3 * sizeof(ushort);
+
+ ///
+ /// Reserved. Must always be 0.
+ ///
+ public ushort Reserved = reserved;
+
+ ///
+ /// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid.
+ ///
+ public IconFileType Type = type;
+
+ ///
+ /// Specifies number of images in the file.
+ ///
+ public ushort Count = count;
+
+ public IconDir(IconFileType type)
+ : this(type, 0)
+ {
+ }
+
+ public IconDir(IconFileType type, ushort count)
+ : this(0, type, count)
+ {
+ }
+
+ public static IconDir Parse(ReadOnlySpan data)
+ => MemoryMarshal.Cast(data)[0];
+
+ public readonly unsafe void WriteTo(Stream stream)
+ => stream.Write(MemoryMarshal.Cast([this]));
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
new file mode 100644
index 000000000..eab15dd87
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs
@@ -0,0 +1,60 @@
+// 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));
+
+ ///
+ /// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
+ ///
+ public byte Width;
+
+ ///
+ /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[
+ ///
+ public byte Height;
+
+ ///
+ /// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
+ ///
+ public byte ColorCount;
+
+ ///
+ /// Reserved. Should be 0.
+ ///
+ public byte Reserved;
+
+ ///
+ /// 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.
+ ///
+ public ushort Planes;
+
+ ///
+ /// In ICO format: Specifies bits per pixel.
+ /// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
+ ///
+ public ushort BitCount;
+
+ ///
+ /// Specifies the size of the image's data in bytes
+ ///
+ public uint BytesInRes;
+
+ ///
+ /// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file.
+ ///
+ public uint ImageOffset;
+
+ public static IconDirEntry Parse(in ReadOnlySpan data)
+ => MemoryMarshal.Cast(data)[0];
+
+ public readonly unsafe void WriteTo(in Stream stream)
+ => stream.Write(MemoryMarshal.Cast([this]));
+}
diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
new file mode 100644
index 000000000..243339661
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.Formats.Ico;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing.Processors.Quantization;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+internal abstract class IconEncoderCore : IImageEncoderInternals
+{
+ private readonly QuantizingImageEncoder encoder;
+ private readonly IconFileType iconFileType;
+ private IconDir fileHeader;
+ private EncodingFrameMetadata[]? entries;
+
+ protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType)
+ {
+ this.encoder = encoder;
+ this.iconFileType = iconFileType;
+ }
+
+ public void Encode(
+ Image image,
+ Stream stream,
+ CancellationToken cancellationToken)
+ where TPixel : unmanaged, IPixel
+ {
+ Guard.NotNull(image, nameof(image));
+ Guard.NotNull(stream, nameof(stream));
+
+ // Stream may not at 0.
+ long basePosition = stream.Position;
+ this.InitHeader(image);
+
+ // We don't write the header and entries yet as we need to write the image data first.
+ int dataOffset = IconDir.Size + (IconDirEntry.Size * this.entries.Length);
+ _ = stream.Seek(dataOffset, SeekOrigin.Current);
+
+ for (int i = 0; i < image.Frames.Count; i++)
+ {
+ // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
+ // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
+ ImageFrame frame = image.Frames[i];
+ int width = this.entries[i].Entry.Width;
+ if (width is 0)
+ {
+ width = frame.Width;
+ }
+
+ int height = this.entries[i].Entry.Height;
+ if (height is 0)
+ {
+ height = frame.Height;
+ }
+
+ this.entries[i].Entry.ImageOffset = (uint)stream.Position;
+
+ // We crop the frame to the size specified in the metadata.
+ // TODO: we can optimize this by cropping the frame only if the new size is both required and different.
+ using Image encodingFrame = new(width, height);
+ for (int y = 0; y < height; y++)
+ {
+ frame.PixelBuffer.DangerousGetRowSpan(y)[..width]
+ .CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y));
+ }
+
+ ref EncodingFrameMetadata encodingMetadata = ref this.entries[i];
+
+ QuantizingImageEncoder encoder = encodingMetadata.Compression switch
+ {
+ IconFrameCompression.Bmp => new BmpEncoder()
+ {
+ Quantizer = this.GetQuantizer(encodingMetadata),
+ ProcessedAlphaMask = true,
+ UseDoubleHeight = true,
+ SkipFileHeader = true,
+ SupportTransparency = false,
+ BitsPerPixel = encodingMetadata.BmpBitsPerPixel
+ },
+ IconFrameCompression.Png => new PngEncoder()
+ {
+ // Only 32bit Png supported.
+ // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
+ BitDepth = PngBitDepth.Bit8,
+ ColorType = PngColorType.RgbWithAlpha,
+ CompressionLevel = PngCompressionLevel.BestCompression
+ },
+ _ => throw new NotSupportedException(),
+ };
+
+ encoder.Encode(encodingFrame, stream);
+ encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset;
+ }
+
+ // We now need to rewind the stream and write the header and the entries.
+ long endPosition = stream.Position;
+ _ = stream.Seek(basePosition, SeekOrigin.Begin);
+ this.fileHeader.WriteTo(stream);
+ foreach (EncodingFrameMetadata frame in this.entries)
+ {
+ frame.Entry.WriteTo(stream);
+ }
+
+ _ = stream.Seek(endPosition, SeekOrigin.Begin);
+ }
+
+ [MemberNotNull(nameof(entries))]
+ private void InitHeader(Image image)
+ {
+ this.fileHeader = new(this.iconFileType, (ushort)image.Frames.Count);
+ this.entries = this.iconFileType switch
+ {
+ IconFileType.ICO =>
+ image.Frames.Select(i =>
+ {
+ IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
+ return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry());
+ }).ToArray(),
+ IconFileType.CUR =>
+ image.Frames.Select(i =>
+ {
+ CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
+ return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry());
+ }).ToArray(),
+ _ => throw new NotSupportedException(),
+ };
+ }
+
+ private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata)
+ {
+ if (metadata.Entry.BitCount > 8)
+ {
+ return null;
+ }
+
+ if (this.encoder.Quantizer is not null)
+ {
+ return this.encoder.Quantizer;
+ }
+
+ if (metadata.ColorTable is null)
+ {
+ return new WuQuantizer(new()
+ {
+ MaxColors = metadata.Entry.ColorCount
+ });
+ }
+
+ // Don't dither if we have a palette. We want to preserve as much information as possible.
+ return new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null });
+ }
+
+ internal sealed class EncodingFrameMetadata
+ {
+ private IconDirEntry iconDirEntry;
+
+ public EncodingFrameMetadata(
+ IconFrameCompression compression,
+ BmpBitsPerPixel bmpBitsPerPixel,
+ ReadOnlyMemory? colorTable,
+ IconDirEntry iconDirEntry)
+ {
+ this.Compression = compression;
+ this.BmpBitsPerPixel = compression == IconFrameCompression.Png
+ ? BmpBitsPerPixel.Pixel32
+ : bmpBitsPerPixel;
+ this.ColorTable = colorTable;
+ this.iconDirEntry = iconDirEntry;
+ }
+
+ public IconFrameCompression Compression { get; }
+
+ public BmpBitsPerPixel BmpBitsPerPixel { get; }
+
+ public ReadOnlyMemory? ColorTable { get; set; }
+
+ public ref IconDirEntry Entry => ref this.iconDirEntry;
+ }
+}
diff --git a/src/ImageSharp/Formats/Icon/IconFileType.cs b/src/ImageSharp/Formats/Icon/IconFileType.cs
new file mode 100644
index 000000000..3450698f1
--- /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 000000000..5c772c3fe
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// IconFrameCompression
+///
+public enum IconFrameCompression
+{
+ ///
+ /// Bmp
+ ///
+ Bmp,
+
+ ///
+ /// Png
+ ///
+ Png
+}
diff --git a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
new file mode 100644
index 000000000..9e7d22de2
--- /dev/null
+++ b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace SixLabors.ImageSharp.Formats.Icon;
+
+///
+/// Detects ico file headers.
+///
+public class IconImageFormatDetector : IImageFormatDetector
+{
+ ///
+ public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size;
+
+ ///
+ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format)
+ {
+ format = this.IsSupportedFileFormat(header) switch
+ {
+ true => Ico.IcoFormat.Instance,
+ false => Cur.CurFormat.Instance,
+ null => default
+ };
+
+ return format is not null;
+ }
+
+ private bool? IsSupportedFileFormat(ReadOnlySpan header)
+ {
+ // There are no magic bytes in the first few bytes of a tga file,
+ // so we try to figure out if its a valid tga by checking for valid tga header bytes.
+ if (header.Length < this.HeaderSize)
+ {
+ return null;
+ }
+
+ IconDir dir = IconDir.Parse(header);
+ if (dir is not { Reserved: 0 } // Should be 0.
+ or not { Type: IconFileType.ICO or IconFileType.CUR } // Unknown Type.
+ or { Count: 0 })
+ {
+ return null;
+ }
+
+ IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]);
+ if (entry is not { Reserved: 0 } // Should be 0.
+ or { BytesInRes: 0 } // Should not be 0.
+ || entry.ImageOffset < IconDir.Size + (dir.Count * IconDirEntry.Size))
+ {
+ return null;
+ }
+
+ if (dir.Type is IconFileType.ICO)
+ {
+ if (entry is not { BitCount: 1 or 4 or 8 or 16 or 24 or 32 } or not { Planes: 0 or 1 })
+ {
+ return null;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 36a0a8bcb..3e278be14 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -345,9 +345,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
uint frameCount = 0;
ImageMetadata metadata = new();
+ List framesMetadata = [];
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
- FrameControl? lastFrameControl = null;
+ FrameControl? currentFrameControl = null;
Span buffer = stackalloc byte[20];
this.currentStream.Skip(8);
@@ -400,7 +401,8 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
break;
}
- lastFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+ currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
+
break;
case PngChunkType.FrameData:
if (frameCount == this.maxFrames)
@@ -413,22 +415,35 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
goto EOF;
}
- if (lastFrameControl is null)
+ if (currentFrameControl is null)
{
PngThrowHelper.ThrowMissingFrameControl();
}
+ InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
+
// Skip sequence number
this.currentStream.Skip(4);
this.SkipChunkDataAndCrc(chunk);
break;
case PngChunkType.Data:
+
// Spec says tRNS must be before IDAT so safe to exit.
if (this.colorMetadataOnly)
{
goto EOF;
}
+ pngMetadata.AnimateRootFrame = currentFrameControl != null;
+ currentFrameControl ??= new((uint)this.header.Width, (uint)this.header.Height);
+ if (framesMetadata.Count == 0)
+ {
+ InitializeFrameMetadata(framesMetadata, currentFrameControl.Value);
+
+ // Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
+ AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
+ }
+
this.SkipChunkDataAndCrc(chunk);
break;
case PngChunkType.Palette:
@@ -515,7 +530,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
// Both PLTE and tRNS chunks, if present, have been read at this point as per spec.
AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata);
- return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata);
+ return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata, framesMetadata);
}
finally
{
@@ -680,6 +695,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
this.scanline = this.configuration.MemoryAllocator.Allocate(this.bytesPerScanline, AllocationOptions.Clean);
}
+ private static void InitializeFrameMetadata(List imageFrameMetadata, FrameControl currentFrameControl)
+ {
+ ImageFrameMetadata meta = new();
+ PngFrameMetadata frameMetadata = meta.GetPngMetadata();
+ frameMetadata.FromChunk(currentFrameControl);
+ imageFrameMetadata.Add(meta);
+ }
+
///
/// Calculates the correct number of bits per pixel for the given color type.
///
diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
index 1c0330d5d..562e47803 100644
--- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs
@@ -99,6 +99,11 @@ public sealed class ImageFrameMetadata : IDeepCloneable
return newMeta;
}
+ internal void SetFormatMetadata(IImageFormat key, TFormatFrameMetadata value)
+ where TFormatMetadata : class
+ where TFormatFrameMetadata : class, IDeepCloneable
+ => this.formatMetadata[key] = value;
+
///
/// Gets the metadata value associated with the specified key.
///
diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs
index 6b62be08f..e811cc1f7 100644
--- a/src/ImageSharp/Metadata/ImageMetadata.cs
+++ b/src/ImageSharp/Metadata/ImageMetadata.cs
@@ -216,6 +216,10 @@ public sealed class ImageMetadata : IDeepCloneable
return false;
}
+ internal void SetFormatMetadata(IImageFormat key, TFormatMetadata value)
+ where TFormatMetadata : class, IDeepCloneable
+ => this.formatMetadata[key] = value;
+
///
public ImageMetadata DeepClone() => new(this);
diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
index acd179ffc..13a59a26d 100644
--- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
+++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
@@ -62,9 +62,10 @@ public class PaletteQuantizer : IQuantizer
{
Guard.NotNull(options, nameof(options));
- // Always use the palette length over options since the palette cannot be reduced.
- TPixel[] palette = new TPixel[this.colorPalette.Length];
- Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
+ // If the palette is larger than the max colors then we need to trim it down.
+ // treat the buffer as FILO.
+ TPixel[] palette = new TPixel[Math.Min(options.MaxColors, this.colorPalette.Length)];
+ Color.ToPixel(this.colorPalette.Span[..palette.Length], palette.AsSpan());
return new PaletteQuantizer(configuration, options, palette, this.transparentIndex);
}
}
diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs
index c5d61726c..c8e6cd265 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 000000000..4efd33648
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.Formats.Icon;
+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);
+
+ CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata();
+ Assert.Equal(image.Width, meta.EncodingWidth);
+ Assert.Equal(image.Height, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(CurFake, PixelTypes.Rgba32)]
+ [WithFile(CurReal, PixelTypes.Rgba32)]
+ public void CurDecoder_Decode2(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(CurDecoder.Instance);
+ CurFrameMetadata meta = image.Frames[0].Metadata.GetCurMetadata();
+ Assert.Equal(image.Width, meta.EncodingWidth);
+ Assert.Equal(image.Height, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
new file mode 100644
index 000000000..b9b66296d
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using static SixLabors.ImageSharp.Tests.TestImages.Cur;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
+
+[Trait("Format", "Cur")]
+public class CurEncoderTests
+{
+ private static CurEncoder Encoder => new();
+
+ [Theory]
+ [WithFile(CurReal, PixelTypes.Rgba32)]
+ [WithFile(WindowsMouse, PixelTypes.Rgba32)]
+ public void CanRoundTripEncoder(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(CurDecoder.Instance);
+ using MemoryStream memStream = new();
+ image.DebugSaveMultiFrame(provider);
+
+ image.Save(memStream, Encoder);
+ memStream.Seek(0, SeekOrigin.Begin);
+
+ using Image encoded = Image.Load(memStream);
+ encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false);
+
+ encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance);
+ }
+}
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 000000000..a776a637b
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
@@ -0,0 +1,332 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Bmp;
+using SixLabors.ImageSharp.Formats.Ico;
+using SixLabors.ImageSharp.Formats.Icon;
+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.DebugSaveMultiFrame(provider);
+
+ Assert.Equal(10, image.Frames.Count);
+ }
+
+ [Theory]
+ [WithFile(Bpp1Size15x15, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size16x16, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size17x17, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size1x1, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size256x256, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size2x2, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size31x31, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size32x32, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size33x33, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size3x3, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size4x4, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size5x5, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size6x6, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size7x7, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size8x8, PixelTypes.Rgba32)]
+ [WithFile(Bpp1Size9x9, PixelTypes.Rgba32)]
+ [WithFile(Bpp1TranspNotSquare, PixelTypes.Rgba32)]
+ [WithFile(Bpp1TranspPartial, PixelTypes.Rgba32)]
+ public void Bpp1Test(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel1, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(Bpp24Size15x15, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size16x16, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size17x17, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size1x1, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size256x256, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size2x2, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size31x31, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size32x32, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size33x33, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size3x3, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size4x4, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size5x5, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size6x6, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size7x7, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size8x8, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Size9x9, PixelTypes.Rgba32)]
+ [WithFile(Bpp24TranspNotSquare, PixelTypes.Rgba32)]
+ [WithFile(Bpp24TranspPartial, PixelTypes.Rgba32)]
+ [WithFile(Bpp24Transp, PixelTypes.Rgba32)]
+ public void Bpp24Test(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel24, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(Bpp32Size15x15, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size16x16, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size17x17, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size1x1, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size256x256, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size2x2, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size31x31, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size32x32, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size33x33, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size3x3, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size4x4, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size5x5, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size6x6, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size7x7, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size8x8, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Size9x9, PixelTypes.Rgba32)]
+ [WithFile(Bpp32TranspNotSquare, PixelTypes.Rgba32)]
+ [WithFile(Bpp32TranspPartial, PixelTypes.Rgba32)]
+ [WithFile(Bpp32Transp, PixelTypes.Rgba32)]
+ public void Bpp32Test(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(Bpp4Size15x15, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size16x16, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size17x17, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size1x1, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size256x256, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size2x2, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size31x31, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size32x32, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size33x33, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size3x3, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size4x4, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size5x5, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size6x6, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size7x7, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size8x8, PixelTypes.Rgba32)]
+ [WithFile(Bpp4Size9x9, PixelTypes.Rgba32)]
+ [WithFile(Bpp4TranspNotSquare, PixelTypes.Rgba32)]
+ [WithFile(Bpp4TranspPartial, PixelTypes.Rgba32)]
+ public void Bpp4Test(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel4, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(Bpp8Size15x15, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size16x16, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size17x17, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size1x1, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size256x256, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size2x2, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size31x31, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size32x32, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size33x33, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size3x3, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size4x4, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size5x5, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size6x6, PixelTypes.Rgba32)]
+ [WithFile(Bpp8Size7x7, PixelTypes.Rgba32)]
+
+ // [WithFile(Bpp8Size8x8, PixelTypes.Rgba32)] This is actually 24 bit.
+ [WithFile(Bpp8Size9x9, PixelTypes.Rgba32)]
+ [WithFile(Bpp8TranspNotSquare, PixelTypes.Rgba32)]
+ [WithFile(Bpp8TranspPartial, PixelTypes.Rgba32)]
+ public void Bpp8Test(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel8, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(InvalidAll, PixelTypes.Rgba32)]
+ [WithFile(InvalidBpp, PixelTypes.Rgba32)]
+ [WithFile(InvalidCompression, PixelTypes.Rgba32)]
+ [WithFile(InvalidRLE4, PixelTypes.Rgba32)]
+ [WithFile(InvalidRLE8, PixelTypes.Rgba32)]
+ public void InvalidTest(TestImageProvider provider)
+ => Assert.Throws(() =>
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+ });
+
+ [Theory]
+ [WithFile(InvalidPng, PixelTypes.Rgba32)]
+ public void InvalidPngTest(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Png, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+
+ [Theory]
+ [WithFile(MixedBmpPngA, PixelTypes.Rgba32)]
+ [WithFile(MixedBmpPngB, PixelTypes.Rgba32)]
+ [WithFile(MixedBmpPngC, PixelTypes.Rgba32)]
+ public void MixedBmpPngTest(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ Assert.True(image.Frames.Count > 1);
+
+ image.DebugSaveMultiFrame(provider);
+ }
+
+ [Theory]
+ [WithFile(MultiSizeA, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeB, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeC, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeD, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeE, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeF, PixelTypes.Rgba32)]
+ public void MultiSizeTest(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ Assert.True(image.Frames.Count > 1);
+
+ for (int i = 0; i < image.Frames.Count; i++)
+ {
+ ImageFrame frame = image.Frames[i];
+ IcoFrameMetadata meta = frame.Metadata.GetIcoMetadata();
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+
+ image.DebugSaveMultiFrame(provider);
+ }
+
+ [Theory]
+ [WithFile(MultiSizeA, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeB, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeC, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeD, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeE, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeF, PixelTypes.Rgba32)]
+ public void MultiSize_CanDecodeSingleFrame(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance, new() { MaxFrames = 1 });
+ Assert.Single(image.Frames);
+ }
+
+ [Theory]
+ [InlineData(MultiSizeA)]
+ [InlineData(MultiSizeB)]
+ [InlineData(MultiSizeC)]
+ [InlineData(MultiSizeD)]
+ [InlineData(MultiSizeE)]
+ [InlineData(MultiSizeF)]
+ public void MultiSize_CanIdentifySingleFrame(string imagePath)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using MemoryStream stream = new(testFile.Bytes, false);
+
+ ImageInfo imageInfo = Image.Identify(new() { MaxFrames = 1 }, stream);
+
+ Assert.Single(imageInfo.FrameMetadataCollection);
+ }
+
+ [Theory]
+ [WithFile(MultiSizeMultiBitsA, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeMultiBitsB, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeMultiBitsC, PixelTypes.Rgba32)]
+ [WithFile(MultiSizeMultiBitsD, PixelTypes.Rgba32)]
+ public void MultiSizeMultiBitsTest(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ Assert.True(image.Frames.Count > 1);
+
+ image.DebugSaveMultiFrame(provider);
+ }
+
+ [Theory]
+ [WithFile(IcoFake, PixelTypes.Rgba32)]
+ public void IcoFakeTest(TestImageProvider provider)
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+
+ image.DebugSave(provider);
+
+ IcoFrameMetadata meta = image.Frames.RootFrame.Metadata.GetIcoMetadata();
+ int expectedWidth = image.Width >= 256 ? 0 : image.Width;
+ int expectedHeight = image.Height >= 256 ? 0 : image.Height;
+
+ Assert.Equal(expectedWidth, meta.EncodingWidth);
+ Assert.Equal(expectedHeight, meta.EncodingHeight);
+ Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
+ Assert.Equal(BmpBitsPerPixel.Pixel32, meta.BmpBitsPerPixel);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
new file mode 100644
index 000000000..db28f9f70
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Ico;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
+using static SixLabors.ImageSharp.Tests.TestImages.Ico;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
+
+[Trait("Format", "Icon")]
+public class IcoEncoderTests
+{
+ private static IcoEncoder Encoder => new();
+
+ [Theory]
+ [WithFile(Flutter, PixelTypes.Rgba32)]
+ public void CanRoundTripEncoder(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(IcoDecoder.Instance);
+ using MemoryStream memStream = new();
+ image.DebugSaveMultiFrame(provider);
+
+ image.Save(memStream, Encoder);
+ memStream.Seek(0, SeekOrigin.Begin);
+
+ using Image encoded = Image.Load(memStream);
+ encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false);
+
+ // Despite preservation of the palette. The process can still be lossy
+ encoded.CompareToOriginalMultiFrame(provider, ImageComparer.TolerantPercentage(.23f), IcoDecoder.Instance);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs
index bf24ba350..2f96b6d43 100644
--- a/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs
@@ -9,6 +9,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga;
[Trait("Format", "Tga")]
public class TgaFileHeaderTests
{
+ // TODO: Some of these clash with the ICO magic bytes. Check correctness.
+ // https://en.wikipedia.org/wiki/Truevision_TGA#Header
[Theory]
[InlineData(new byte[] { 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 195, 0, 32, 8 })] // invalid tga image type.
[InlineData(new byte[] { 0, 3, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 250, 0, 195, 0, 32, 8 })] // invalid colormap type.
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 7aaaac6f8..8937799e1 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1122,4 +1122,128 @@ public static class TestImages
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 const string Bpp1Size15x15 = "Icon/1bpp_size_15x15.ico";
+ public const string Bpp1Size16x16 = "Icon/1bpp_size_16x16.ico";
+ public const string Bpp1Size17x17 = "Icon/1bpp_size_17x17.ico";
+ public const string Bpp1Size1x1 = "Icon/1bpp_size_1x1.ico";
+ public const string Bpp1Size256x256 = "Icon/1bpp_size_256x256.ico";
+ public const string Bpp1Size2x2 = "Icon/1bpp_size_2x2.ico";
+ public const string Bpp1Size31x31 = "Icon/1bpp_size_31x31.ico";
+ public const string Bpp1Size32x32 = "Icon/1bpp_size_32x32.ico";
+ public const string Bpp1Size33x33 = "Icon/1bpp_size_33x33.ico";
+ public const string Bpp1Size3x3 = "Icon/1bpp_size_3x3.ico";
+ public const string Bpp1Size4x4 = "Icon/1bpp_size_4x4.ico";
+ public const string Bpp1Size5x5 = "Icon/1bpp_size_5x5.ico";
+ public const string Bpp1Size6x6 = "Icon/1bpp_size_6x6.ico";
+ public const string Bpp1Size7x7 = "Icon/1bpp_size_7x7.ico";
+ public const string Bpp1Size8x8 = "Icon/1bpp_size_8x8.ico";
+ public const string Bpp1Size9x9 = "Icon/1bpp_size_9x9.ico";
+ public const string Bpp1TranspNotSquare = "Icon/1bpp_transp_not_square.ico";
+ public const string Bpp1TranspPartial = "Icon/1bpp_transp_partial.ico";
+ public const string Bpp24Size15x15 = "Icon/24bpp_size_15x15.ico";
+ public const string Bpp24Size16x16 = "Icon/24bpp_size_16x16.ico";
+ public const string Bpp24Size17x17 = "Icon/24bpp_size_17x17.ico";
+ public const string Bpp24Size1x1 = "Icon/24bpp_size_1x1.ico";
+ public const string Bpp24Size256x256 = "Icon/24bpp_size_256x256.ico";
+ public const string Bpp24Size2x2 = "Icon/24bpp_size_2x2.ico";
+ public const string Bpp24Size31x31 = "Icon/24bpp_size_31x31.ico";
+ public const string Bpp24Size32x32 = "Icon/24bpp_size_32x32.ico";
+ public const string Bpp24Size33x33 = "Icon/24bpp_size_33x33.ico";
+ public const string Bpp24Size3x3 = "Icon/24bpp_size_3x3.ico";
+ public const string Bpp24Size4x4 = "Icon/24bpp_size_4x4.ico";
+ public const string Bpp24Size5x5 = "Icon/24bpp_size_5x5.ico";
+ public const string Bpp24Size6x6 = "Icon/24bpp_size_6x6.ico";
+ public const string Bpp24Size7x7 = "Icon/24bpp_size_7x7.ico";
+ public const string Bpp24Size8x8 = "Icon/24bpp_size_8x8.ico";
+ public const string Bpp24Size9x9 = "Icon/24bpp_size_9x9.ico";
+ public const string Bpp24TranspNotSquare = "Icon/24bpp_transp_not_square.ico";
+ public const string Bpp24TranspPartial = "Icon/24bpp_transp_partial.ico";
+ public const string Bpp24Transp = "Icon/24bpp_transp.ico";
+ public const string Bpp32Size15x15 = "Icon/32bpp_size_15x15.ico";
+ public const string Bpp32Size16x16 = "Icon/32bpp_size_16x16.ico";
+ public const string Bpp32Size17x17 = "Icon/32bpp_size_17x17.ico";
+ public const string Bpp32Size1x1 = "Icon/32bpp_size_1x1.ico";
+ public const string Bpp32Size256x256 = "Icon/32bpp_size_256x256.ico";
+ public const string Bpp32Size2x2 = "Icon/32bpp_size_2x2.ico";
+ public const string Bpp32Size31x31 = "Icon/32bpp_size_31x31.ico";
+ public const string Bpp32Size32x32 = "Icon/32bpp_size_32x32.ico";
+ public const string Bpp32Size33x33 = "Icon/32bpp_size_33x33.ico";
+ public const string Bpp32Size3x3 = "Icon/32bpp_size_3x3.ico";
+ public const string Bpp32Size4x4 = "Icon/32bpp_size_4x4.ico";
+ public const string Bpp32Size5x5 = "Icon/32bpp_size_5x5.ico";
+ public const string Bpp32Size6x6 = "Icon/32bpp_size_6x6.ico";
+ public const string Bpp32Size7x7 = "Icon/32bpp_size_7x7.ico";
+ public const string Bpp32Size8x8 = "Icon/32bpp_size_8x8.ico";
+ public const string Bpp32Size9x9 = "Icon/32bpp_size_9x9.ico";
+ public const string Bpp32TranspNotSquare = "Icon/32bpp_transp_not_square.ico";
+ public const string Bpp32TranspPartial = "Icon/32bpp_transp_partial.ico";
+ public const string Bpp32Transp = "Icon/32bpp_transp.ico";
+ public const string Bpp4Size15x15 = "Icon/4bpp_size_15x15.ico";
+ public const string Bpp4Size16x16 = "Icon/4bpp_size_16x16.ico";
+ public const string Bpp4Size17x17 = "Icon/4bpp_size_17x17.ico";
+ public const string Bpp4Size1x1 = "Icon/4bpp_size_1x1.ico";
+ public const string Bpp4Size256x256 = "Icon/4bpp_size_256x256.ico";
+ public const string Bpp4Size2x2 = "Icon/4bpp_size_2x2.ico";
+ public const string Bpp4Size31x31 = "Icon/4bpp_size_31x31.ico";
+ public const string Bpp4Size32x32 = "Icon/4bpp_size_32x32.ico";
+ public const string Bpp4Size33x33 = "Icon/4bpp_size_33x33.ico";
+ public const string Bpp4Size3x3 = "Icon/4bpp_size_3x3.ico";
+ public const string Bpp4Size4x4 = "Icon/4bpp_size_4x4.ico";
+ public const string Bpp4Size5x5 = "Icon/4bpp_size_5x5.ico";
+ public const string Bpp4Size6x6 = "Icon/4bpp_size_6x6.ico";
+ public const string Bpp4Size7x7 = "Icon/4bpp_size_7x7.ico";
+ public const string Bpp4Size8x8 = "Icon/4bpp_size_8x8.ico";
+ public const string Bpp4Size9x9 = "Icon/4bpp_size_9x9.ico";
+ public const string Bpp4TranspNotSquare = "Icon/4bpp_transp_not_square.ico";
+ public const string Bpp4TranspPartial = "Icon/4bpp_transp_partial.ico";
+ public const string Bpp8Size15x15 = "Icon/8bpp_size_15x15.ico";
+ public const string Bpp8Size16x16 = "Icon/8bpp_size_16x16.ico";
+ public const string Bpp8Size17x17 = "Icon/8bpp_size_17x17.ico";
+ public const string Bpp8Size1x1 = "Icon/8bpp_size_1x1.ico";
+ public const string Bpp8Size256x256 = "Icon/8bpp_size_256x256.ico";
+ public const string Bpp8Size2x2 = "Icon/8bpp_size_2x2.ico";
+ public const string Bpp8Size31x31 = "Icon/8bpp_size_31x31.ico";
+ public const string Bpp8Size32x32 = "Icon/8bpp_size_32x32.ico";
+ public const string Bpp8Size33x33 = "Icon/8bpp_size_33x33.ico";
+ public const string Bpp8Size3x3 = "Icon/8bpp_size_3x3.ico";
+ public const string Bpp8Size4x4 = "Icon/8bpp_size_4x4.ico";
+ public const string Bpp8Size5x5 = "Icon/8bpp_size_5x5.ico";
+ public const string Bpp8Size6x6 = "Icon/8bpp_size_6x6.ico";
+ public const string Bpp8Size7x7 = "Icon/8bpp_size_7x7.ico";
+ public const string Bpp8Size8x8 = "Icon/8bpp_size_8x8.ico";
+ public const string Bpp8Size9x9 = "Icon/8bpp_size_9x9.ico";
+ public const string Bpp8TranspNotSquare = "Icon/8bpp_transp_not_square.ico";
+ public const string Bpp8TranspPartial = "Icon/8bpp_transp_partial.ico";
+ public const string InvalidAll = "Icon/invalid_all.ico";
+ public const string InvalidBpp = "Icon/invalid_bpp.ico";
+ public const string InvalidCompression = "Icon/invalid_compression.ico";
+ public const string InvalidPng = "Icon/invalid_png.ico";
+ public const string InvalidRLE4 = "Icon/invalid_RLE4.ico";
+ public const string InvalidRLE8 = "Icon/invalid_RLE8.ico";
+ public const string MixedBmpPngA = "Icon/mixed_bmp_png_a.ico";
+ public const string MixedBmpPngB = "Icon/mixed_bmp_png_b.ico";
+ public const string MixedBmpPngC = "Icon/mixed_bmp_png_c.ico";
+ public const string MultiSizeA = "Icon/multi_size_a.ico";
+ public const string MultiSizeB = "Icon/multi_size_b.ico";
+ public const string MultiSizeC = "Icon/multi_size_c.ico";
+ public const string MultiSizeD = "Icon/multi_size_d.ico";
+ public const string MultiSizeE = "Icon/multi_size_e.ico";
+ public const string MultiSizeF = "Icon/multi_size_f.ico";
+ public const string MultiSizeMultiBitsA = "Icon/multi_size_multi_bits_a.ico";
+ public const string MultiSizeMultiBitsB = "Icon/multi_size_multi_bits_b.ico";
+ public const string MultiSizeMultiBitsC = "Icon/multi_size_multi_bits_c.ico";
+ public const string MultiSizeMultiBitsD = "Icon/multi_size_multi_bits_d.ico";
+ public const string IcoFake = "Icon/ico_fake.cur";
+ }
+
+ public static class Cur
+ {
+ public const string WindowsMouse = "Icon/aero_arrow.cur";
+ public const string CurReal = "Icon/cur_real.cur";
+ public const string CurFake = "Icon/cur_fake.ico";
+ }
}
diff --git a/tests/Images/Input/Icon/1bpp_size_15x15.ico b/tests/Images/Input/Icon/1bpp_size_15x15.ico
new file mode 100644
index 000000000..39fc9c521
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_15x15.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:846dda605ee23bb641534b272fa57300eacd85038feea5dd1a3f6d4b543a935e
+size 190
diff --git a/tests/Images/Input/Icon/1bpp_size_16x16.ico b/tests/Images/Input/Icon/1bpp_size_16x16.ico
new file mode 100644
index 000000000..6179678bc
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_16x16.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:029d0438eda83d4d9e087cf79abe2d0234728c37f570de808355c0e79c71be17
+size 198
diff --git a/tests/Images/Input/Icon/1bpp_size_17x17.ico b/tests/Images/Input/Icon/1bpp_size_17x17.ico
new file mode 100644
index 000000000..90138a08d
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_17x17.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8db024a49fdd91c9d5d1fc0c1d6f60991518a5776031f58cdafbdd3ed9e4f26b
+size 206
diff --git a/tests/Images/Input/Icon/1bpp_size_1x1.ico b/tests/Images/Input/Icon/1bpp_size_1x1.ico
new file mode 100644
index 000000000..1161a3f3c
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_1x1.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f13d1bbfa5b29bc386270cb492b705eded0825a9eb3a6341f4ea2b3dbe085cd1
+size 78
diff --git a/tests/Images/Input/Icon/1bpp_size_256x256.ico b/tests/Images/Input/Icon/1bpp_size_256x256.ico
new file mode 100644
index 000000000..d6524a31f
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_256x256.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c1aa37daf2fd65b424c5e13e94bed165328e624584cc5664d73bb4030a1e1f12
+size 16454
diff --git a/tests/Images/Input/Icon/1bpp_size_2x2.ico b/tests/Images/Input/Icon/1bpp_size_2x2.ico
new file mode 100644
index 000000000..73394156a
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_2x2.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:18be2a5384812de1bec70733fb0b283a159edc5d0bc03981de8fb3ccddb8911e
+size 86
diff --git a/tests/Images/Input/Icon/1bpp_size_31x31.ico b/tests/Images/Input/Icon/1bpp_size_31x31.ico
new file mode 100644
index 000000000..8dffe659f
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_31x31.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a3a4e7964e3ed5ca2a98929e2f903a6b969961410aab6a935a0c54fbe716d0c3
+size 318
diff --git a/tests/Images/Input/Icon/1bpp_size_32x32.ico b/tests/Images/Input/Icon/1bpp_size_32x32.ico
new file mode 100644
index 000000000..e281eb378
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_32x32.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:06a55f8219234e43beec6776fe15a7d6d4ad314deaf64df115ea45f2100e5283
+size 326
diff --git a/tests/Images/Input/Icon/1bpp_size_33x33.ico b/tests/Images/Input/Icon/1bpp_size_33x33.ico
new file mode 100644
index 000000000..c5e4677d3
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_33x33.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:82fa82f6b954515eace3cdd6d4681abd149b37354a7dee4f0d1966f516f27850
+size 598
diff --git a/tests/Images/Input/Icon/1bpp_size_3x3.ico b/tests/Images/Input/Icon/1bpp_size_3x3.ico
new file mode 100644
index 000000000..89872b959
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_3x3.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5ff0bf1c415925642d99691a9a9a6b92d9995447bc62ed80b9b0c6d4efdcd19b
+size 94
diff --git a/tests/Images/Input/Icon/1bpp_size_4x4.ico b/tests/Images/Input/Icon/1bpp_size_4x4.ico
new file mode 100644
index 000000000..1e47b4596
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_4x4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5853ba73b06e0f2a0c71850011526419db3bd7c76e5b7b2f6b22f748ce919bf2
+size 102
diff --git a/tests/Images/Input/Icon/1bpp_size_5x5.ico b/tests/Images/Input/Icon/1bpp_size_5x5.ico
new file mode 100644
index 000000000..5152c7575
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_5x5.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ecd253a6862ec9a9870c8a8a370528b28c22d23247dfc29e09fab65d95b9416d
+size 110
diff --git a/tests/Images/Input/Icon/1bpp_size_6x6.ico b/tests/Images/Input/Icon/1bpp_size_6x6.ico
new file mode 100644
index 000000000..a1d5c09c0
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_6x6.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a80c7f8d37dc3997bcd674a5af835cae802cacbf5d65020f0aaae67f70cfc31e
+size 118
diff --git a/tests/Images/Input/Icon/1bpp_size_7x7.ico b/tests/Images/Input/Icon/1bpp_size_7x7.ico
new file mode 100644
index 000000000..9c5a227e3
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_7x7.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:00a26274e8563e6378f8bfa4d1aa9695030593a38791ca366cecd3949b0f52af
+size 126
diff --git a/tests/Images/Input/Icon/1bpp_size_8x8.ico b/tests/Images/Input/Icon/1bpp_size_8x8.ico
new file mode 100644
index 000000000..c019914ee
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_8x8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78150aee2f5d5ccd2a172abfe11f1127efb950cb9173e22e380809afb2a94d3c
+size 134
diff --git a/tests/Images/Input/Icon/1bpp_size_9x9.ico b/tests/Images/Input/Icon/1bpp_size_9x9.ico
new file mode 100644
index 000000000..2f3fd28eb
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_size_9x9.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8cecf833b31208710c1dde1bed3322a95453468a3f4b39afdad66ac9bc5f86b
+size 142
diff --git a/tests/Images/Input/Icon/1bpp_transp_not_square.ico b/tests/Images/Input/Icon/1bpp_transp_not_square.ico
new file mode 100644
index 000000000..1c678ec40
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_transp_not_square.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bd9f41711b53d4e1e915ddb992522b97a981fbe3f4536826f0c66e2d6a3677fb
+size 182
diff --git a/tests/Images/Input/Icon/1bpp_transp_partial.ico b/tests/Images/Input/Icon/1bpp_transp_partial.ico
new file mode 100644
index 000000000..6365a53df
--- /dev/null
+++ b/tests/Images/Input/Icon/1bpp_transp_partial.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cedcd221abf4b95115f9f8f34f15da456b09e7e56972cced24a99fa56bf8aca9
+size 326
diff --git a/tests/Images/Input/Icon/24bpp_size_15x15.ico b/tests/Images/Input/Icon/24bpp_size_15x15.ico
new file mode 100644
index 000000000..f8697e2b5
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_15x15.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b506403852d936d14975ed9aba1c50ab3873cbbd81afcf38381f8e5e841fafd0
+size 842
diff --git a/tests/Images/Input/Icon/24bpp_size_16x16.ico b/tests/Images/Input/Icon/24bpp_size_16x16.ico
new file mode 100644
index 000000000..e6de107d7
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_16x16.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8bfa9de0f613e9e82e9037193c4124f87d05ac1390459e2f018da45c16200de6
+size 894
diff --git a/tests/Images/Input/Icon/24bpp_size_17x17.ico b/tests/Images/Input/Icon/24bpp_size_17x17.ico
new file mode 100644
index 000000000..2c37ffa8b
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_17x17.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a4cd2ad22e55000a706d365e0a3395f3e4d9a3933c00880d5f12903ac0aed60e
+size 1014
diff --git a/tests/Images/Input/Icon/24bpp_size_1x1.ico b/tests/Images/Input/Icon/24bpp_size_1x1.ico
new file mode 100644
index 000000000..f9137f61a
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_1x1.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3b20e9a6479c3831b7af75d30bc909ce3046e45384bfd62d7a10ac6816c1c947
+size 70
diff --git a/tests/Images/Input/Icon/24bpp_size_256x256.ico b/tests/Images/Input/Icon/24bpp_size_256x256.ico
new file mode 100644
index 000000000..08f928c8e
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_256x256.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1175641f39e68bd6cd38b8f64f02510b22c5a642afc7503d357b064163b3d37b
+size 204862
diff --git a/tests/Images/Input/Icon/24bpp_size_2x2.ico b/tests/Images/Input/Icon/24bpp_size_2x2.ico
new file mode 100644
index 000000000..d6d472a25
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_2x2.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:72a1380a306b51ae37eabd3edeb0b134b0f8e8e120e6c64b6b08bd833a9c70a4
+size 86
diff --git a/tests/Images/Input/Icon/24bpp_size_31x31.ico b/tests/Images/Input/Icon/24bpp_size_31x31.ico
new file mode 100644
index 000000000..5c585c582
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_31x31.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:574b0971908b44334f1f3be79e5b0ff336e74a8b9947b43a295d3a2b86733965
+size 3162
diff --git a/tests/Images/Input/Icon/24bpp_size_32x32.ico b/tests/Images/Input/Icon/24bpp_size_32x32.ico
new file mode 100644
index 000000000..3663b8767
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_32x32.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa45880e5611436fc06de0603481d0a61044e52a1a053961fdda1139bb5660a6
+size 3262
diff --git a/tests/Images/Input/Icon/24bpp_size_33x33.ico b/tests/Images/Input/Icon/24bpp_size_33x33.ico
new file mode 100644
index 000000000..4834b48c8
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_33x33.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8704a77d6264f6f104e23950099e3d3a4a577787e67c7c39d7925f9d0a347572
+size 3626
diff --git a/tests/Images/Input/Icon/24bpp_size_3x3.ico b/tests/Images/Input/Icon/24bpp_size_3x3.ico
new file mode 100644
index 000000000..f2c11ccfe
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_3x3.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:81229c27fbc684c0da99b1b04189046d5b9f531ee4111ccc43bde6404dce4f12
+size 110
diff --git a/tests/Images/Input/Icon/24bpp_size_4x4.ico b/tests/Images/Input/Icon/24bpp_size_4x4.ico
new file mode 100644
index 000000000..2d7880a03
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_4x4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f7cb620eb83acbf20d308f5c6cb8b0246b8b156cdcc43e39f5809754fd4d5ddb
+size 126
diff --git a/tests/Images/Input/Icon/24bpp_size_5x5.ico b/tests/Images/Input/Icon/24bpp_size_5x5.ico
new file mode 100644
index 000000000..a98c85c19
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_5x5.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bdf1718e3d9695cdf2552e42219d1e3166ad5a94f53cbb44db4a7222e2a32f9a
+size 162
diff --git a/tests/Images/Input/Icon/24bpp_size_6x6.ico b/tests/Images/Input/Icon/24bpp_size_6x6.ico
new file mode 100644
index 000000000..5dd3c57c2
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_6x6.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1e5f3a8f59e297cf67251a89558d4406c4c515c3e3ce7555df4a53ef5420fc38
+size 206
diff --git a/tests/Images/Input/Icon/24bpp_size_7x7.ico b/tests/Images/Input/Icon/24bpp_size_7x7.ico
new file mode 100644
index 000000000..d9622629e
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_7x7.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0ee1b5836c9f8c89e9b8c8873f1ad92f7555ed9706f4dc76345a727bf3e9f334
+size 258
diff --git a/tests/Images/Input/Icon/24bpp_size_8x8.ico b/tests/Images/Input/Icon/24bpp_size_8x8.ico
new file mode 100644
index 000000000..39be58ce4
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_8x8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b46ca32ddb84074d9140224738480eaa0a6c0dce2dbf2074625add1901c27117
+size 286
diff --git a/tests/Images/Input/Icon/24bpp_size_9x9.ico b/tests/Images/Input/Icon/24bpp_size_9x9.ico
new file mode 100644
index 000000000..9e7873eaf
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_size_9x9.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7454d6332f4bdba929d610e4a8a232b1443d5a64119de4e69c00e0f03e55e237
+size 350
diff --git a/tests/Images/Input/Icon/24bpp_transp.ico b/tests/Images/Input/Icon/24bpp_transp.ico
new file mode 100644
index 000000000..a64157a63
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_transp.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb8ea41822350e5f40bac2aef19ec7a4c40561ce6637948b3fa6db7835c1fded
+size 3262
diff --git a/tests/Images/Input/Icon/24bpp_transp_not_square.ico b/tests/Images/Input/Icon/24bpp_transp_not_square.ico
new file mode 100644
index 000000000..5abf2ad66
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_transp_not_square.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:44b4c79ff497df0e99f55d68d82f59c0d7c2f4d5e9bb63bcc1b5910f4a2853db
+size 1126
diff --git a/tests/Images/Input/Icon/24bpp_transp_partial.ico b/tests/Images/Input/Icon/24bpp_transp_partial.ico
new file mode 100644
index 000000000..d1a37498b
--- /dev/null
+++ b/tests/Images/Input/Icon/24bpp_transp_partial.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8d109413d7f699b92e9d6a5e2c52d2b5c747f2ca9ff31d326f8d4ec2fd5840f
+size 3262
diff --git a/tests/Images/Input/Icon/32bpp_size_15x15.ico b/tests/Images/Input/Icon/32bpp_size_15x15.ico
new file mode 100644
index 000000000..a7f94e94d
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_15x15.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:02b5abd8e15ef2fba1a26cdbdf6e8f66abbbf9aa188404ef911a1d2d02b7b050
+size 1022
diff --git a/tests/Images/Input/Icon/32bpp_size_16x16.ico b/tests/Images/Input/Icon/32bpp_size_16x16.ico
new file mode 100644
index 000000000..609a51826
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_16x16.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:50bb07bd12f9b388ba6a3abbb815aaf1c800438e35ec48201269fa23342e5622
+size 1150
diff --git a/tests/Images/Input/Icon/32bpp_size_17x17.ico b/tests/Images/Input/Icon/32bpp_size_17x17.ico
new file mode 100644
index 000000000..13a71140f
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_17x17.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6bf4c701d38fc988186e3fcfee6e426ed5a84807b54b91e4ab8c1b12e0794746
+size 1286
diff --git a/tests/Images/Input/Icon/32bpp_size_1x1.ico b/tests/Images/Input/Icon/32bpp_size_1x1.ico
new file mode 100644
index 000000000..3f449eefe
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_1x1.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f62be892b36609a57c34eb4784dfb6dc82698ecd15709898a6579ee4f21f668e
+size 70
diff --git a/tests/Images/Input/Icon/32bpp_size_256x256.ico b/tests/Images/Input/Icon/32bpp_size_256x256.ico
new file mode 100644
index 000000000..2229aee95
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_256x256.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b15f75adc54e70c4751cf9973854f225ef15b12bdaddb5ab00cb9edfd8b386b1
+size 270398
diff --git a/tests/Images/Input/Icon/32bpp_size_2x2.ico b/tests/Images/Input/Icon/32bpp_size_2x2.ico
new file mode 100644
index 000000000..cbb64292b
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_2x2.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fa859752136cdf055874ae71589f9585aaaeaef804b13ce192991793d9e57e57
+size 86
diff --git a/tests/Images/Input/Icon/32bpp_size_31x31.ico b/tests/Images/Input/Icon/32bpp_size_31x31.ico
new file mode 100644
index 000000000..837e8f512
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_31x31.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5ecf1dc1fdd3dc6cb2fd90b49632b19260e73ad3a6624372d1e9eefc470ed6b
+size 4030
diff --git a/tests/Images/Input/Icon/32bpp_size_32x32.ico b/tests/Images/Input/Icon/32bpp_size_32x32.ico
new file mode 100644
index 000000000..b359d4d84
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_32x32.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:395ccdc596487ed63db4d893d03b6aa24743b37279d896196d8d38e7028415ea
+size 4286
diff --git a/tests/Images/Input/Icon/32bpp_size_33x33.ico b/tests/Images/Input/Icon/32bpp_size_33x33.ico
new file mode 100644
index 000000000..01df9ae7d
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_33x33.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e8b89c061abcf959b90149585cc34237aad19e1f67c162d62e5adbad4826970
+size 4682
diff --git a/tests/Images/Input/Icon/32bpp_size_3x3.ico b/tests/Images/Input/Icon/32bpp_size_3x3.ico
new file mode 100644
index 000000000..8879d3f1f
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_3x3.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:52b7772a9c8fae189abc3cf37d5d24bcdfe94077e6b1cf7be8cc7e0bc6f6bddf
+size 110
diff --git a/tests/Images/Input/Icon/32bpp_size_4x4.ico b/tests/Images/Input/Icon/32bpp_size_4x4.ico
new file mode 100644
index 000000000..d800b2198
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_4x4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2bb0831fab10fb0ff9a0b2b2ea137609a45a6ec3982d594878996eafc32836ad
+size 142
diff --git a/tests/Images/Input/Icon/32bpp_size_5x5.ico b/tests/Images/Input/Icon/32bpp_size_5x5.ico
new file mode 100644
index 000000000..710c38a23
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_5x5.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:10e2ed5cbacc761f2d467c22a8d72dcc4086b9423c5487ba05826804c642730a
+size 182
diff --git a/tests/Images/Input/Icon/32bpp_size_6x6.ico b/tests/Images/Input/Icon/32bpp_size_6x6.ico
new file mode 100644
index 000000000..4223c1fc2
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_6x6.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1ab79a308d24592557b368f00566999d777e38de385eb6ebabc90d53ac723ae9
+size 230
diff --git a/tests/Images/Input/Icon/32bpp_size_7x7.ico b/tests/Images/Input/Icon/32bpp_size_7x7.ico
new file mode 100644
index 000000000..1e321acb6
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_7x7.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3799164811b0284535fa90ca60f13e9c0754ca4e8f00d96a83951e91e748d760
+size 286
diff --git a/tests/Images/Input/Icon/32bpp_size_8x8.ico b/tests/Images/Input/Icon/32bpp_size_8x8.ico
new file mode 100644
index 000000000..b44fe22d5
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_8x8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2a3187750c9313024f983fdfca270f5db2c5d730831adfce0fb19ddac25deecc
+size 350
diff --git a/tests/Images/Input/Icon/32bpp_size_9x9.ico b/tests/Images/Input/Icon/32bpp_size_9x9.ico
new file mode 100644
index 000000000..682b148ed
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_size_9x9.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1ab8bd47cc2d2ce0e9c1a5810b5ccbe3f46e35001114c18f34cbf51ff0566bf6
+size 422
diff --git a/tests/Images/Input/Icon/32bpp_transp.ico b/tests/Images/Input/Icon/32bpp_transp.ico
new file mode 100644
index 000000000..592536290
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_transp.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b4f94718304fa41041b8cebad7b76762d410b366b46d620f5599b03a2fa7ba00
+size 4286
diff --git a/tests/Images/Input/Icon/32bpp_transp_not_square.ico b/tests/Images/Input/Icon/32bpp_transp_not_square.ico
new file mode 100644
index 000000000..3a0bb3dd0
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_transp_not_square.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6e3c2061b64df989e8ae68aee993eba5e11d03e23f282f063cc3929ec3ef2b0c
+size 1462
diff --git a/tests/Images/Input/Icon/32bpp_transp_partial.ico b/tests/Images/Input/Icon/32bpp_transp_partial.ico
new file mode 100644
index 000000000..334a0b75c
--- /dev/null
+++ b/tests/Images/Input/Icon/32bpp_transp_partial.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5bd34021ec302f203c39d038854607d9fe8bcd3133ea57b65ebc6da81aa8a4b
+size 4286
diff --git a/tests/Images/Input/Icon/4bpp_size_15x15.ico b/tests/Images/Input/Icon/4bpp_size_15x15.ico
new file mode 100644
index 000000000..ce67e54cc
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_15x15.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dd1fd73648c1b7acc4aef4e0c4e6672ab7241e47cb52345c4459aa86185f4dfd
+size 306
diff --git a/tests/Images/Input/Icon/4bpp_size_16x16.ico b/tests/Images/Input/Icon/4bpp_size_16x16.ico
new file mode 100644
index 000000000..f26d88b36
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_16x16.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e5f8547e5e6aae3a2f662fa266d0f78731d310fb051f99dce5693d6adcbfbb4f
+size 318
diff --git a/tests/Images/Input/Icon/4bpp_size_17x17.ico b/tests/Images/Input/Icon/4bpp_size_17x17.ico
new file mode 100644
index 000000000..aa44dd4f1
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_17x17.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1fd0286a127371d4b40a5c30c6b221753a711231e386b636fcb07d2f1f92967f
+size 398
diff --git a/tests/Images/Input/Icon/4bpp_size_1x1.ico b/tests/Images/Input/Icon/4bpp_size_1x1.ico
new file mode 100644
index 000000000..7049b0b36
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_1x1.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:313f969c26d12cae6e778563d1c6c9df248c5d28ff657ad5054a280e06573106
+size 134
diff --git a/tests/Images/Input/Icon/4bpp_size_256x256.ico b/tests/Images/Input/Icon/4bpp_size_256x256.ico
new file mode 100644
index 000000000..fa0740065
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_256x256.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dcad369c1e56dd01b8644a5903ad25af19cc78047b5e0821546d1171a0ab31ff
+size 41086
diff --git a/tests/Images/Input/Icon/4bpp_size_2x2.ico b/tests/Images/Input/Icon/4bpp_size_2x2.ico
new file mode 100644
index 000000000..2b8d74afa
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_2x2.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11b1142401678412ce8718dd6e943fab05ec7974c7ae36286316e7f4d168f0f5
+size 142
diff --git a/tests/Images/Input/Icon/4bpp_size_31x31.ico b/tests/Images/Input/Icon/4bpp_size_31x31.ico
new file mode 100644
index 000000000..86b71f36c
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_31x31.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b19f60f3441b268a19be0f99078d079c4eb416b882118071ad43ceff41ca40b
+size 746
diff --git a/tests/Images/Input/Icon/4bpp_size_32x32.ico b/tests/Images/Input/Icon/4bpp_size_32x32.ico
new file mode 100644
index 000000000..aa8fc0932
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_32x32.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7f0863b486575dca2d5e3ce07da10cb30e039524f17026d9668d75bad780e833
+size 766
diff --git a/tests/Images/Input/Icon/4bpp_size_33x33.ico b/tests/Images/Input/Icon/4bpp_size_33x33.ico
new file mode 100644
index 000000000..ad93685f2
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_33x33.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64b77f80fb48237a0f9cfbd808289e53ed7a70b107f190c93d76d32342829ccb
+size 1050
diff --git a/tests/Images/Input/Icon/4bpp_size_3x3.ico b/tests/Images/Input/Icon/4bpp_size_3x3.ico
new file mode 100644
index 000000000..781266a67
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_3x3.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f8cb358defec66494d3be4eb01fd7e304edfd04435b4552d51769d0858659686
+size 150
diff --git a/tests/Images/Input/Icon/4bpp_size_4x4.ico b/tests/Images/Input/Icon/4bpp_size_4x4.ico
new file mode 100644
index 000000000..ffe599149
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_4x4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:53083ae340dcc04d88acaf9d75a2dc68b23500c294eb3ed4e15e3fba86c5843f
+size 158
diff --git a/tests/Images/Input/Icon/4bpp_size_5x5.ico b/tests/Images/Input/Icon/4bpp_size_5x5.ico
new file mode 100644
index 000000000..70f2db036
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_5x5.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e4f1401d87e285b18e6f818dfb00a3fb275e72e2fd4bce6652bfdd74bf9565f3
+size 166
diff --git a/tests/Images/Input/Icon/4bpp_size_6x6.ico b/tests/Images/Input/Icon/4bpp_size_6x6.ico
new file mode 100644
index 000000000..230d8e85f
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_6x6.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a3fa6bdd6125a9de6d3ddaafd5e62e0435b14c52f82239a6d36d44aaf5a49361
+size 174
diff --git a/tests/Images/Input/Icon/4bpp_size_7x7.ico b/tests/Images/Input/Icon/4bpp_size_7x7.ico
new file mode 100644
index 000000000..7c4b9834b
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_7x7.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c4025a4bd2dde619f950f69d38e042ae38d2a1840d7b00540c372546b5d8ccb0
+size 182
diff --git a/tests/Images/Input/Icon/4bpp_size_8x8.ico b/tests/Images/Input/Icon/4bpp_size_8x8.ico
new file mode 100644
index 000000000..b1f3050bf
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_8x8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:332563676808485aac8e2635baacad3f4d1ce5fc64c6be07daa25962f4fa1687
+size 190
diff --git a/tests/Images/Input/Icon/4bpp_size_9x9.ico b/tests/Images/Input/Icon/4bpp_size_9x9.ico
new file mode 100644
index 000000000..fc7751086
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_size_9x9.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:da5169395108194503853db2f431e0d02b98a191a1bf597c31fe269e7d84206a
+size 234
diff --git a/tests/Images/Input/Icon/4bpp_transp_not_square.ico b/tests/Images/Input/Icon/4bpp_transp_not_square.ico
new file mode 100644
index 000000000..6b2babe8e
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_transp_not_square.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2dede473b7a427029cdb319ecf04aaf71cf8ce1d806efb65d7e3844ebd1703f9
+size 350
diff --git a/tests/Images/Input/Icon/4bpp_transp_partial.ico b/tests/Images/Input/Icon/4bpp_transp_partial.ico
new file mode 100644
index 000000000..4394b9e43
--- /dev/null
+++ b/tests/Images/Input/Icon/4bpp_transp_partial.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7e0617f49eb057ab0346203e0cd04fd4e93de71053bbbfaa3c9655696b10a80d
+size 766
diff --git a/tests/Images/Input/Icon/8bpp_size_15x15.ico b/tests/Images/Input/Icon/8bpp_size_15x15.ico
new file mode 100644
index 000000000..086edb745
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_15x15.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:577c86aa8b78cca86885ed20b702b260150eacf738beb18da28c25bcb01cfdcd
+size 1386
diff --git a/tests/Images/Input/Icon/8bpp_size_16x16.ico b/tests/Images/Input/Icon/8bpp_size_16x16.ico
new file mode 100644
index 000000000..e09d74447
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_16x16.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9900b6783aac800836e19890f11fda1bf1d2138dee63403adbc79bf207a71dfd
+size 1406
diff --git a/tests/Images/Input/Icon/8bpp_size_17x17.ico b/tests/Images/Input/Icon/8bpp_size_17x17.ico
new file mode 100644
index 000000000..9a5684b4d
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_17x17.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5b1c214eb0fd9f5b1f1da4ceeb98a3cc0d7b6b02d7ed6aaa5138078e48bf8613
+size 1494
diff --git a/tests/Images/Input/Icon/8bpp_size_1x1.ico b/tests/Images/Input/Icon/8bpp_size_1x1.ico
new file mode 100644
index 000000000..20961847a
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_1x1.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:032c11a8e5aaa5f9beb997c83504233b5ad278d3a4e129cd384a4ca62950a998
+size 1094
diff --git a/tests/Images/Input/Icon/8bpp_size_256x256.ico b/tests/Images/Input/Icon/8bpp_size_256x256.ico
new file mode 100644
index 000000000..59b0bb63b
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_256x256.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7aa8f4fc33c541f88318d7c36a5b5e964cb0470c541677790b49b85638c63fd6
+size 74814
diff --git a/tests/Images/Input/Icon/8bpp_size_2x2.ico b/tests/Images/Input/Icon/8bpp_size_2x2.ico
new file mode 100644
index 000000000..bcbdf9c07
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_2x2.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6e1c2b72b6ddf0cfedd9e20e5abf3ae3fd19066be811251bb9db404e6dabe279
+size 1102
diff --git a/tests/Images/Input/Icon/8bpp_size_31x31.ico b/tests/Images/Input/Icon/8bpp_size_31x31.ico
new file mode 100644
index 000000000..7c320d183
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_31x31.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9a588e6f1031c1c8f0d66b9eef770d01b3f79aaea4203d58ee07255c1c6ee258
+size 2238
diff --git a/tests/Images/Input/Icon/8bpp_size_32x32.ico b/tests/Images/Input/Icon/8bpp_size_32x32.ico
new file mode 100644
index 000000000..af7581f35
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_32x32.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6b00ab33cbb42e9f499844e5add713fcc83854cc2cb104d5c9bd04a456662099
+size 2238
diff --git a/tests/Images/Input/Icon/8bpp_size_33x33.ico b/tests/Images/Input/Icon/8bpp_size_33x33.ico
new file mode 100644
index 000000000..3c8043fe6
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_33x33.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e356bb9200375f8fcbc5a0ed70353194a7b1432133e0c61f0b2f03bca3888080
+size 2538
diff --git a/tests/Images/Input/Icon/8bpp_size_3x3.ico b/tests/Images/Input/Icon/8bpp_size_3x3.ico
new file mode 100644
index 000000000..075791998
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_3x3.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:13cccd62e97bbe6cdd6ca5c4bc765794c7babd3638ff4d09b116a62c3c50be56
+size 1110
diff --git a/tests/Images/Input/Icon/8bpp_size_4x4.ico b/tests/Images/Input/Icon/8bpp_size_4x4.ico
new file mode 100644
index 000000000..af95b287d
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_4x4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4343544be11a37083219d99d2dfb4a16b02c309d54223897d6c71503414cc2ef
+size 1118
diff --git a/tests/Images/Input/Icon/8bpp_size_5x5.ico b/tests/Images/Input/Icon/8bpp_size_5x5.ico
new file mode 100644
index 000000000..2e4f80495
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_5x5.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b7cfdf4a0d696573a4cb5291e3165c98444692f1d03a3cab1630df33c765ecd9
+size 1146
diff --git a/tests/Images/Input/Icon/8bpp_size_6x6.ico b/tests/Images/Input/Icon/8bpp_size_6x6.ico
new file mode 100644
index 000000000..65ca70be2
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_6x6.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d8d36eb593ee34d22e5ff99a08f2b177dbb92355f85210b2a9e53d7ce9e2cc6b
+size 1158
diff --git a/tests/Images/Input/Icon/8bpp_size_7x7.ico b/tests/Images/Input/Icon/8bpp_size_7x7.ico
new file mode 100644
index 000000000..e02e8fb37
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_7x7.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:29050464e324a70bb8a41fb5732b96dbf3bcb36a74ca29775e1f37b07b9ee582
+size 1170
diff --git a/tests/Images/Input/Icon/8bpp_size_8x8.ico b/tests/Images/Input/Icon/8bpp_size_8x8.ico
new file mode 100644
index 000000000..39be58ce4
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_8x8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b46ca32ddb84074d9140224738480eaa0a6c0dce2dbf2074625add1901c27117
+size 286
diff --git a/tests/Images/Input/Icon/8bpp_size_9x9.ico b/tests/Images/Input/Icon/8bpp_size_9x9.ico
new file mode 100644
index 000000000..8819490ae
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_size_9x9.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ddeaa63b1ec9d389c140499e2b5d2e911a57d8f37467696bb9e9ec3e60ec77b
+size 1230
diff --git a/tests/Images/Input/Icon/8bpp_transp_not_square.ico b/tests/Images/Input/Icon/8bpp_transp_not_square.ico
new file mode 100644
index 000000000..6b6bbdc9f
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_transp_not_square.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04a255b4cd43ec7005a9a946c2b3dba57eba709b16be85ef77e553943d35f745
+size 1478
diff --git a/tests/Images/Input/Icon/8bpp_transp_partial.ico b/tests/Images/Input/Icon/8bpp_transp_partial.ico
new file mode 100644
index 000000000..73cd34c73
--- /dev/null
+++ b/tests/Images/Input/Icon/8bpp_transp_partial.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:20abca3096b955bc91ed95d194ff3f856473d6d372b5bea0d8c75fd20a231a26
+size 2238
diff --git a/tests/Images/Input/Icon/aero_arrow.cur b/tests/Images/Input/Icon/aero_arrow.cur
new file mode 100644
index 000000000..82cbbd33e
--- /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/cur_fake.ico b/tests/Images/Input/Icon/cur_fake.ico
new file mode 100644
index 000000000..cad7542c8
--- /dev/null
+++ b/tests/Images/Input/Icon/cur_fake.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6679016e7954e335cef537630d122cc3a7a05cb2f3ef32f72811d724b83d4c28
+size 4286
diff --git a/tests/Images/Input/Icon/cur_real.cur b/tests/Images/Input/Icon/cur_real.cur
new file mode 100644
index 000000000..cad7542c8
--- /dev/null
+++ b/tests/Images/Input/Icon/cur_real.cur
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6679016e7954e335cef537630d122cc3a7a05cb2f3ef32f72811d724b83d4c28
+size 4286
diff --git a/tests/Images/Input/Icon/flutter.ico b/tests/Images/Input/Icon/flutter.ico
new file mode 100644
index 000000000..4001f1426
--- /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
diff --git a/tests/Images/Input/Icon/ico_fake.cur b/tests/Images/Input/Icon/ico_fake.cur
new file mode 100644
index 000000000..5ab040538
--- /dev/null
+++ b/tests/Images/Input/Icon/ico_fake.cur
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d3a3185dcf0a2c3f5d3fe821474f6787c4de7cffe08db6bd730073ad94e7538
+size 4286
diff --git a/tests/Images/Input/Icon/invalid_RLE4.ico b/tests/Images/Input/Icon/invalid_RLE4.ico
new file mode 100644
index 000000000..5d2e25fd3
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_RLE4.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:43c543a1c66608a89f0b187afa1526af4be4f7c94265897002b3a150328b964e
+size 86
diff --git a/tests/Images/Input/Icon/invalid_RLE8.ico b/tests/Images/Input/Icon/invalid_RLE8.ico
new file mode 100644
index 000000000..2ebefbf33
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_RLE8.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b55b3f3f87d9d5801150ffd999c62623528a33d031ca8d6fe665be3328d8c94d
+size 86
diff --git a/tests/Images/Input/Icon/invalid_all.ico b/tests/Images/Input/Icon/invalid_all.ico
new file mode 100644
index 000000000..ca34a2578
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_all.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:93c4f059ced481667315dd7b90e9c1beed0a42a08f3ff8e51e2388919fafa79a
+size 283
diff --git a/tests/Images/Input/Icon/invalid_bpp.ico b/tests/Images/Input/Icon/invalid_bpp.ico
new file mode 100644
index 000000000..913f780ed
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_bpp.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:396bfd08531fc0eae8b5e364ef4c62035e8493a3f59286dedbca6f441d8e9690
+size 86
diff --git a/tests/Images/Input/Icon/invalid_compression.ico b/tests/Images/Input/Icon/invalid_compression.ico
new file mode 100644
index 000000000..7e697d3d2
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_compression.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f051ed80db684748d2b2669c77b95068e209b2fe2917fa65f6218beef4dcead5
+size 830
diff --git a/tests/Images/Input/Icon/invalid_png.ico b/tests/Images/Input/Icon/invalid_png.ico
new file mode 100644
index 000000000..cbd394fc6
--- /dev/null
+++ b/tests/Images/Input/Icon/invalid_png.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:61bc9e74d8fd9f72c8ccaf9a3887c517e17c0a39d9d41acabc3699be545b9703
+size 901
diff --git a/tests/Images/Input/Icon/mixed_bmp_png_a.ico b/tests/Images/Input/Icon/mixed_bmp_png_a.ico
new file mode 100644
index 000000000..f35027255
--- /dev/null
+++ b/tests/Images/Input/Icon/mixed_bmp_png_a.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:47adfa4e36adf74ae49cfa481cb54cffe659c09d4b52765e973b6adc8cc31e97
+size 3653
diff --git a/tests/Images/Input/Icon/mixed_bmp_png_b.ico b/tests/Images/Input/Icon/mixed_bmp_png_b.ico
new file mode 100644
index 000000000..3efdcab74
--- /dev/null
+++ b/tests/Images/Input/Icon/mixed_bmp_png_b.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:96338768d8f9c90a6af94268dc55d94809a412c3164c381926b3759ffbf2df79
+size 45693
diff --git a/tests/Images/Input/Icon/mixed_bmp_png_c.ico b/tests/Images/Input/Icon/mixed_bmp_png_c.ico
new file mode 100644
index 000000000..65b504eef
--- /dev/null
+++ b/tests/Images/Input/Icon/mixed_bmp_png_c.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e9be0358616581f45eddeaee474c0ce0be8a82279428f5ef1890b2fb6f0c0d27
+size 164189
diff --git a/tests/Images/Input/Icon/multi_size_a.ico b/tests/Images/Input/Icon/multi_size_a.ico
new file mode 100644
index 000000000..c34fdc638
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_a.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:dba75ec62f5785ce5d16c2c5c04637b18ccb164917092373b3d470326e7bc0c4
+size 17542
diff --git a/tests/Images/Input/Icon/multi_size_b.ico b/tests/Images/Input/Icon/multi_size_b.ico
new file mode 100644
index 000000000..2065bd638
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_b.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1d7ac6313ee263103f4ab6aa5147b1d85bf5ff792c0980189aac9bfab1288011
+size 99678
diff --git a/tests/Images/Input/Icon/multi_size_c.ico b/tests/Images/Input/Icon/multi_size_c.ico
new file mode 100644
index 000000000..c6ee58297
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_c.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:85e04b807e084bc9a0e1a65289e21ca1baac1e70c3a37fabbea7d69995945f08
+size 202850
diff --git a/tests/Images/Input/Icon/multi_size_d.ico b/tests/Images/Input/Icon/multi_size_d.ico
new file mode 100644
index 000000000..3d9fc96fb
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_d.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4a0ce27b63b386c4fdbf7835db738f0e98f274f5a112b7bd45c32dfab93952a0
+size 216804
diff --git a/tests/Images/Input/Icon/multi_size_e.ico b/tests/Images/Input/Icon/multi_size_e.ico
new file mode 100644
index 000000000..8f2991acb
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_e.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:363ff3655e978ffe30a8cefec3bd4202a1ae0a22f3ab48e56362f56a31fb349f
+size 372526
diff --git a/tests/Images/Input/Icon/multi_size_f.ico b/tests/Images/Input/Icon/multi_size_f.ico
new file mode 100644
index 000000000..99948cf1e
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_f.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:04701ce87eb82280a4e53d816a0ac3ee91ebc28b1959641bddb90787015ff4a8
+size 3084
diff --git a/tests/Images/Input/Icon/multi_size_multi_bits_a.ico b/tests/Images/Input/Icon/multi_size_multi_bits_a.ico
new file mode 100644
index 000000000..12b2bf66c
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_multi_bits_a.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a1561795509c9e8dbf0633db9b4c242e72e0ebe4ae9a7718328e96b6b273c3ca
+size 4710
diff --git a/tests/Images/Input/Icon/multi_size_multi_bits_b.ico b/tests/Images/Input/Icon/multi_size_multi_bits_b.ico
new file mode 100644
index 000000000..599168aea
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_multi_bits_b.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8fe15c1a8ca4bae0ad588863418af960fb62def2db4abfcae594de4fc1b2304a
+size 31134
diff --git a/tests/Images/Input/Icon/multi_size_multi_bits_c.ico b/tests/Images/Input/Icon/multi_size_multi_bits_c.ico
new file mode 100644
index 000000000..701b574dc
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_multi_bits_c.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c5afa3da7d278c1d2272581971117408c38ec5fe3aaac17e5234f7a8012fd9e2
+size 72513
diff --git a/tests/Images/Input/Icon/multi_size_multi_bits_d.ico b/tests/Images/Input/Icon/multi_size_multi_bits_d.ico
new file mode 100644
index 000000000..271ec92a5
--- /dev/null
+++ b/tests/Images/Input/Icon/multi_size_multi_bits_d.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e0754e570ab3a2ce81759aa206fc6e8780fb1024fb20e60dbdb329ee4c0c1831
+size 293950