Browse Source

Merge branch 'main' into js/colorspace-converter

pull/2739/head
James Jackson-South 2 years ago
committed by GitHub
parent
commit
f4f1eed3ac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      ImageSharp.sln
  2. 6
      src/ImageSharp/Configuration.cs
  3. 7
      src/ImageSharp/Formats/Bmp/BmpConstants.cs
  4. 235
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  5. 24
      src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs
  6. 9
      src/ImageSharp/Formats/Bmp/BmpEncoder.cs
  7. 90
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  8. 14
      src/ImageSharp/Formats/Bmp/BmpMetadata.cs
  9. 20
      src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
  10. 39
      src/ImageSharp/Formats/Cur/CurConstants.cs
  11. 47
      src/ImageSharp/Formats/Cur/CurDecoder.cs
  12. 30
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  13. 17
      src/ImageSharp/Formats/Cur/CurEncoder.cs
  14. 14
      src/ImageSharp/Formats/Cur/CurEncoderCore.cs
  15. 37
      src/ImageSharp/Formats/Cur/CurFormat.cs
  16. 100
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  17. 16
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  18. 45
      src/ImageSharp/Formats/Cur/MetadataExtensions.cs
  19. 20
      src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
  20. 39
      src/ImageSharp/Formats/Ico/IcoConstants.cs
  21. 47
      src/ImageSharp/Formats/Ico/IcoDecoder.cs
  22. 30
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  23. 17
      src/ImageSharp/Formats/Ico/IcoEncoder.cs
  24. 14
      src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
  25. 37
      src/ImageSharp/Formats/Ico/IcoFormat.cs
  26. 95
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  27. 16
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  28. 45
      src/ImageSharp/Formats/Ico/MetadataExtensions.cs
  29. 306
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  30. 43
      src/ImageSharp/Formats/Icon/IconDir.cs
  31. 60
      src/ImageSharp/Formats/Icon/IconDirEntry.cs
  32. 184
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  33. 20
      src/ImageSharp/Formats/Icon/IconFileType.cs
  34. 20
      src/ImageSharp/Formats/Icon/IconFrameCompression.cs
  35. 66
      src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
  36. 31
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  37. 5
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  38. 4
      src/ImageSharp/Metadata/ImageMetadata.cs
  39. 7
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  40. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  41. 41
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  42. 34
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
  43. 332
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  44. 34
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs
  45. 2
      tests/ImageSharp.Tests/Formats/Tga/TgaFileHeaderTests.cs
  46. 124
      tests/ImageSharp.Tests/TestImages.cs
  47. 3
      tests/Images/Input/Icon/1bpp_size_15x15.ico
  48. 3
      tests/Images/Input/Icon/1bpp_size_16x16.ico
  49. 3
      tests/Images/Input/Icon/1bpp_size_17x17.ico
  50. 3
      tests/Images/Input/Icon/1bpp_size_1x1.ico
  51. 3
      tests/Images/Input/Icon/1bpp_size_256x256.ico
  52. 3
      tests/Images/Input/Icon/1bpp_size_2x2.ico
  53. 3
      tests/Images/Input/Icon/1bpp_size_31x31.ico
  54. 3
      tests/Images/Input/Icon/1bpp_size_32x32.ico
  55. 3
      tests/Images/Input/Icon/1bpp_size_33x33.ico
  56. 3
      tests/Images/Input/Icon/1bpp_size_3x3.ico
  57. 3
      tests/Images/Input/Icon/1bpp_size_4x4.ico
  58. 3
      tests/Images/Input/Icon/1bpp_size_5x5.ico
  59. 3
      tests/Images/Input/Icon/1bpp_size_6x6.ico
  60. 3
      tests/Images/Input/Icon/1bpp_size_7x7.ico
  61. 3
      tests/Images/Input/Icon/1bpp_size_8x8.ico
  62. 3
      tests/Images/Input/Icon/1bpp_size_9x9.ico
  63. 3
      tests/Images/Input/Icon/1bpp_transp_not_square.ico
  64. 3
      tests/Images/Input/Icon/1bpp_transp_partial.ico
  65. 3
      tests/Images/Input/Icon/24bpp_size_15x15.ico
  66. 3
      tests/Images/Input/Icon/24bpp_size_16x16.ico
  67. 3
      tests/Images/Input/Icon/24bpp_size_17x17.ico
  68. 3
      tests/Images/Input/Icon/24bpp_size_1x1.ico
  69. 3
      tests/Images/Input/Icon/24bpp_size_256x256.ico
  70. 3
      tests/Images/Input/Icon/24bpp_size_2x2.ico
  71. 3
      tests/Images/Input/Icon/24bpp_size_31x31.ico
  72. 3
      tests/Images/Input/Icon/24bpp_size_32x32.ico
  73. 3
      tests/Images/Input/Icon/24bpp_size_33x33.ico
  74. 3
      tests/Images/Input/Icon/24bpp_size_3x3.ico
  75. 3
      tests/Images/Input/Icon/24bpp_size_4x4.ico
  76. 3
      tests/Images/Input/Icon/24bpp_size_5x5.ico
  77. 3
      tests/Images/Input/Icon/24bpp_size_6x6.ico
  78. 3
      tests/Images/Input/Icon/24bpp_size_7x7.ico
  79. 3
      tests/Images/Input/Icon/24bpp_size_8x8.ico
  80. 3
      tests/Images/Input/Icon/24bpp_size_9x9.ico
  81. 3
      tests/Images/Input/Icon/24bpp_transp.ico
  82. 3
      tests/Images/Input/Icon/24bpp_transp_not_square.ico
  83. 3
      tests/Images/Input/Icon/24bpp_transp_partial.ico
  84. 3
      tests/Images/Input/Icon/32bpp_size_15x15.ico
  85. 3
      tests/Images/Input/Icon/32bpp_size_16x16.ico
  86. 3
      tests/Images/Input/Icon/32bpp_size_17x17.ico
  87. 3
      tests/Images/Input/Icon/32bpp_size_1x1.ico
  88. 3
      tests/Images/Input/Icon/32bpp_size_256x256.ico
  89. 3
      tests/Images/Input/Icon/32bpp_size_2x2.ico
  90. 3
      tests/Images/Input/Icon/32bpp_size_31x31.ico
  91. 3
      tests/Images/Input/Icon/32bpp_size_32x32.ico
  92. 3
      tests/Images/Input/Icon/32bpp_size_33x33.ico
  93. 3
      tests/Images/Input/Icon/32bpp_size_3x3.ico
  94. 3
      tests/Images/Input/Icon/32bpp_size_4x4.ico
  95. 3
      tests/Images/Input/Icon/32bpp_size_5x5.ico
  96. 3
      tests/Images/Input/Icon/32bpp_size_6x6.ico
  97. 3
      tests/Images/Input/Icon/32bpp_size_7x7.ico
  98. 3
      tests/Images/Input/Icon/32bpp_size_8x8.ico
  99. 3
      tests/Images/Input/Icon/32bpp_size_9x9.ico
  100. 3
      tests/Images/Input/Icon/32bpp_transp.ico

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

6
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());
}

7
src/ImageSharp/Formats/Bmp/BmpConstants.cs

@ -11,7 +11,12 @@ internal static class BmpConstants
/// <summary>
/// The list of mimetypes that equate to a bmp.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/bmp", "image/x-windows-bmp" };
public static readonly IEnumerable<string> MimeTypes = new[]
{
"image/bmp",
"image/x-windows-bmp",
"image/x-win-bitmap"
};
/// <summary>
/// The list of file extensions that equate to a bmp.

235
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
/// <summary>
/// The file header containing general information.
/// </summary>
private BmpFileHeader fileHeader;
private BmpFileHeader? fileHeader;
/// <summary>
/// Indicates which bitmap file marker was read.
@ -99,6 +100,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// </summary>
private readonly RleSkippedPixelHandling rleSkippedPixelHandling;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask;
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
private readonly bool skipFileHeader;
/// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
private readonly bool isDoubleHeight;
/// <summary>
/// Initializes a new instance of the <see cref="BmpDecoderCore"/> class.
/// </summary>
@ -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;
}
/// <inheritdoc />
@ -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
}
}
/// <inheritdoc cref="ReadRgbPalette"/>
private void ReadRgbPaletteWithAlphaMask<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
where TPixel : unmanaged, IPixel<TPixel>
{
// 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<byte> row = this.memoryAllocator.Allocate<byte>(arrayWidth + padding, AllocationOptions.Clean))
{
Span<byte> 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<byte, Bgr24>(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<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
for (int x = 0; x < width; x++)
{
pixelRow[x] = TPixel.FromBgra32(image[newY, x]);
}
}
}
/// <summary>
/// Set pixel's alpha with alpha mask.
/// </summary>
/// <param name="pixel">Bgra32 pixel.</param>
/// <param name="mask">alpha mask.</param>
/// <param name="index">bit index of pixel.</param>
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;
}
/// <summary>
/// Reads the 16 bit color palette from the stream.
/// </summary>
@ -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<Bgr24>()];
ReadOnlySpan<Bgr24> rgbTable = MemoryMarshal.Cast<byte, Bgr24>(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.");

24
src/ImageSharp/Formats/Bmp/BmpDecoderOptions.cs

@ -16,4 +16,28 @@ public sealed class BmpDecoderOptions : ISpecializedDecoderOptions
/// which can occur during decoding run length encoded bitmaps.
/// </summary>
public RleSkippedPixelHandling RleSkippedPixelHandling { get; init; }
/// <summary>
/// Gets a value indicating whether the additional alpha mask is processed at decoding time.
/// </summary>
/// <remarks>
/// Used by the icon decoder.
/// </remarks>
internal bool ProcessedAlphaMask { get; init; }
/// <summary>
/// Gets a value indicating whether to skip loading the BMP file header.
/// </summary>
/// <remarks>
/// Used by the icon decoder.
/// </remarks>
internal bool SkipFileHeader { get; init; }
/// <summary>
/// Gets a value indicating whether to treat the height as double of true height.
/// </summary>
/// <remarks>
/// Used by the icon decoder.
/// </remarks>
internal bool UseDoubleHeight { get; init; }
}

9
src/ImageSharp/Formats/Bmp/BmpEncoder.cs

@ -29,6 +29,15 @@ public sealed class BmpEncoder : QuantizingImageEncoder
/// </summary>
public bool SupportTransparency { get; init; }
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
internal bool ProcessedAlphaMask { get; init; }
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
internal bool SkipFileHeader { get; init; }
/// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
internal bool UseDoubleHeight { get; init; }
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{

90
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
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <inheritdoc cref="BmpDecoderOptions.ProcessedAlphaMask"/>
private readonly bool processedAlphaMask;
/// <inheritdoc cref="BmpDecoderOptions.SkipFileHeader"/>
private readonly bool skipFileHeader;
/// <inheritdoc cref="BmpDecoderOptions.UseDoubleHeight"/>
private readonly bool isDoubleHeight;
/// <summary>
/// Initializes a new instance of the <see cref="BmpEncoderCore"/> class.
/// </summary>
@ -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;
}
/// <summary>
@ -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<byte> 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
/// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileData">The color profile data.</param>
/// <param name="buffer">The buffer.</param>
private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span<byte> buffer)
/// <param name="basePosition">The Stream may not be start with 0.</param>
private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span<byte> 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<byte> AllocateRow(int width, int bytesPerPixel)
@ -722,4 +759,45 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
stream.WriteByte(indices);
}
private static void ProcessedAlphaMask<TPixel>(Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>
{
int arrayWidth = image.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
{
padding = 4 - padding;
}
Span<byte> mask = stackalloc byte[arrayWidth];
for (int y = image.Height - 1; y >= 0; y--)
{
mask.Clear();
Span<TPixel> 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<TPixel>(in TPixel pixel, ref byte mask, in int index)
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)
{
mask |= unchecked((byte)(0b10000000 >> index));
}
}
}

14
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();
}
}
/// <summary>
@ -35,8 +40,11 @@ public class BmpMetadata : IDeepCloneable
/// </summary>
public BmpBitsPerPixel BitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel24;
/// <summary>
/// Gets or sets the color table, if any.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new BmpMetadata(this);
// TODO: Colors used once we support encoding palette bmps.
}

20
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// </summary>
public sealed class CurConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
}

39
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;
/// <summary>
/// Defines constants relating to ICOs
/// </summary>
internal static class CurConstants
{
/// <summary>
/// The list of mime types that equate to a cur.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
/// </remarks>
public static readonly IEnumerable<string> 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",
];
/// <summary>
/// The list of file extensions that equate to a cur.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["cur"];
public const uint FileHeader = 0x00_02_00_00;
}

47
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;
/// <summary>
/// Decoder for generating an image out of a ico encoded stream.
/// </summary>
public sealed class CurDecoder : ImageDecoder
{
private CurDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static CurDecoder Instance { get; } = new();
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
Image<TPixel> image = new CurDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
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);
}
}

30
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<Color>? colorTable)
{
CurFrameMetadata curFrameMetadata = metadata.GetCurMetadata();
curFrameMetadata.FromIconDirEntry(entry);
curFrameMetadata.Compression = compression;
curFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
curFrameMetadata.ColorTable = colorTable;
}
}

17
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;
/// <summary>
/// Image encoder for writing an image to a stream as a Windows Cursor.
/// </summary>
public sealed class CurEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
CurEncoderCore encoderCore = new(this);
encoderCore.Encode(image, stream, cancellationToken);
}
}

14
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)
{
}
}

37
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the ICO format.
/// </summary>
public sealed class CurFormat : IImageFormat<CurMetadata, CurFrameMetadata>
{
private CurFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static CurFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "ICO";
/// <inheritdoc/>
public string DefaultMimeType => CurConstants.MimeTypes.First();
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => CurConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => CurConstants.FileExtensions;
/// <inheritdoc/>
public CurMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public CurFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}

100
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;
/// <summary>
/// IcoFrameMetadata.
/// </summary>
public class CurFrameMetadata : IDeepCloneable<CurFrameMetadata>, IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
/// </summary>
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;
}
/// <summary>
/// Gets or sets the frame compressions format.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the horizontal coordinates of the hotspot in number of pixels from the left.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical coordinates of the hotspot in number of pixels from the top.
/// </summary>
public ushort HotspotY { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
/// <summary>
/// Gets or sets the color table, if any.
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
/// <inheritdoc/>
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
};
}
}

16
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;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// </summary>
public class CurMetadata : IDeepCloneable<CurMetadata>, IDeepCloneable
{
/// <inheritdoc/>
public CurMetadata DeepClone() => new();
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
}

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

20
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// </summary>
public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
}

39
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;
/// <summary>
/// Defines constants relating to ICOs
/// </summary>
internal static class IcoConstants
{
/// <summary>
/// The list of mime types that equate to a ico.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
/// </remarks>
public static readonly IEnumerable<string> 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",
];
/// <summary>
/// The list of file extensions that equate to a ico.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["ico"];
public const uint FileHeader = 0x00_01_00_00;
}

47
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;
/// <summary>
/// Decoder for generating an image out of a ico encoded stream.
/// </summary>
public sealed class IcoDecoder : ImageDecoder
{
private IcoDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static IcoDecoder Instance { get; } = new();
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
Image<TPixel> image = new IcoDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
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);
}
}

30
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<Color>? colorTable)
{
IcoFrameMetadata icoFrameMetadata = metadata.GetIcoMetadata();
icoFrameMetadata.FromIconDirEntry(entry);
icoFrameMetadata.Compression = compression;
icoFrameMetadata.BmpBitsPerPixel = bitsPerPixel;
icoFrameMetadata.ColorTable = colorTable;
}
}

17
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;
/// <summary>
/// Image encoder for writing an image to a stream as a Windows Icon.
/// </summary>
public sealed class IcoEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
IcoEncoderCore encoderCore = new(this);
encoderCore.Encode(image, stream, cancellationToken);
}
}

14
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)
{
}
}

37
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the ICO format.
/// </summary>
public sealed class IcoFormat : IImageFormat<IcoMetadata, IcoFrameMetadata>
{
private IcoFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static IcoFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "ICO";
/// <inheritdoc/>
public string DefaultMimeType => IcoConstants.MimeTypes.First();
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => IcoConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => IcoConstants.FileExtensions;
/// <inheritdoc/>
public IcoMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public IcoFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}

95
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;
/// <summary>
/// Provides Ico specific metadata information for the image frame.
/// </summary>
public class IcoFrameMetadata : IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
/// </summary>
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();
}
}
/// <summary>
/// Gets or sets the frame compressions format.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.<br/>
/// Used when <see cref="Compression"/> is <see cref="IconFrameCompression.Bmp"/>
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Pixel32;
/// <summary>
/// Gets or sets the color table, if any.
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
/// <inheritdoc/>
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,
},
};
}
}

16
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;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// </summary>
public class IcoMetadata : IDeepCloneable<IcoMetadata>, IDeepCloneable
{
/// <inheritdoc/>
public IcoMetadata DeepClone() => new();
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
}

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

306
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<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Stream may not at 0.
long basePosition = stream.Position;
this.ReadHeader(stream);
Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length];
List<(Image<TPixel> 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<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(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<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Pixel32;
ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> 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<byte> 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<Color>? 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<Color>? colorTable);
[MemberNotNull(nameof(entries))]
protected void ReadHeader(Stream stream)
{
Span<byte> 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;
}
}

43
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);
/// <summary>
/// Reserved. Must always be 0.
/// </summary>
public ushort Reserved = reserved;
/// <summary>
/// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid.
/// </summary>
public IconFileType Type = type;
/// <summary>
/// Specifies number of images in the file.
/// </summary>
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<byte> data)
=> MemoryMarshal.Cast<byte, IconDir>(data)[0];
public readonly unsafe void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDir, byte>([this]));
}

60
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));
/// <summary>
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
/// </summary>
public byte Width;
/// <summary>
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[
/// </summary>
public byte Height;
/// <summary>
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
/// </summary>
public byte ColorCount;
/// <summary>
/// Reserved. Should be 0.
/// </summary>
public byte Reserved;
/// <summary>
/// In ICO format: Specifies color planes. Should be 0 or 1.<br/>
/// In CUR format: Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
/// </summary>
public ushort Planes;
/// <summary>
/// In ICO format: Specifies bits per pixel.<br/>
/// In CUR format: Specifies the vertical coordinates of the hotspot in number of pixels from the top.
/// </summary>
public ushort BitCount;
/// <summary>
/// Specifies the size of the image's data in bytes
/// </summary>
public uint BytesInRes;
/// <summary>
/// Specifies the offset of BMP or PNG data from the beginning of the ICO/CUR file.
/// </summary>
public uint ImageOffset;
public static IconDirEntry Parse(in ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDirEntry>(data)[0];
public readonly unsafe void WriteTo(in Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDirEntry, byte>([this]));
}

184
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<TPixel>(
Image<TPixel> image,
Stream stream,
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
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<TPixel> 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<TPixel> 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<Color>? 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<Color>? ColorTable { get; set; }
public ref IconDirEntry Entry => ref this.iconDirEntry;
}
}

20
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;
/// <summary>
/// Ico file type
/// </summary>
internal enum IconFileType : ushort
{
/// <summary>
/// ICO file
/// </summary>
ICO = 1,
/// <summary>
/// CUR file
/// </summary>
CUR = 2,
}

20
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;
/// <summary>
/// IconFrameCompression
/// </summary>
public enum IconFrameCompression
{
/// <summary>
/// Bmp
/// </summary>
Bmp,
/// <summary>
/// Png
/// </summary>
Png
}

66
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;
/// <summary>
/// Detects ico file headers.
/// </summary>
public class IconImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> 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<byte> 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;
}
}

31
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -345,9 +345,10 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
{
uint frameCount = 0;
ImageMetadata metadata = new();
List<ImageFrameMetadata> framesMetadata = [];
PngMetadata pngMetadata = metadata.GetPngMetadata();
this.currentStream = stream;
FrameControl? lastFrameControl = null;
FrameControl? currentFrameControl = null;
Span<byte> 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<byte>(this.bytesPerScanline, AllocationOptions.Clean);
}
private static void InitializeFrameMetadata(List<ImageFrameMetadata> imageFrameMetadata, FrameControl currentFrameControl)
{
ImageFrameMetadata meta = new();
PngFrameMetadata frameMetadata = meta.GetPngMetadata();
frameMetadata.FromChunk(currentFrameControl);
imageFrameMetadata.Add(meta);
}
/// <summary>
/// Calculates the correct number of bits per pixel for the given color type.
/// </summary>

5
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -99,6 +99,11 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
return newMeta;
}
internal void SetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, TFormatFrameMetadata value)
where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable
=> this.formatMetadata[key] = value;
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>

4
src/ImageSharp/Metadata/ImageMetadata.cs

@ -216,6 +216,10 @@ public sealed class ImageMetadata : IDeepCloneable<ImageMetadata>
return false;
}
internal void SetFormatMetadata<TFormatMetadata>(IImageFormat<TFormatMetadata> key, TFormatMetadata value)
where TFormatMetadata : class, IDeepCloneable
=> this.formatMetadata[key] = value;
/// <inheritdoc/>
public ImageMetadata DeepClone() => new(this);

7
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<TPixel>(configuration, options, palette, this.transparentIndex);
}
}

2
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()
{

41
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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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);
}
}

34
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(CurDecoder.Instance);
using MemoryStream memStream = new();
image.DebugSaveMultiFrame(provider);
image.Save(memStream, Encoder);
memStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> encoded = Image.Load<TPixel>(memStream);
encoded.DebugSaveMultiFrame(provider, appendPixelTypeToFileName: false);
encoded.CompareToOriginalMultiFrame(provider, ImageComparer.Exact, CurDecoder.Instance);
}
}

332
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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
=> Assert.Throws<NotSupportedException>(() =>
{
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
});
[Theory]
[WithFile(InvalidPng, PixelTypes.Rgba32)]
public void InvalidPngTest(TestImageProvider<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
Assert.True(image.Frames.Count > 1);
for (int i = 0; i < image.Frames.Count; i++)
{
ImageFrame<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> 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<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
Assert.True(image.Frames.Count > 1);
image.DebugSaveMultiFrame(provider);
}
[Theory]
[WithFile(IcoFake, PixelTypes.Rgba32)]
public void IcoFakeTest(TestImageProvider<Rgba32> provider)
{
using Image<Rgba32> 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);
}
}

34
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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(IcoDecoder.Instance);
using MemoryStream memStream = new();
image.DebugSaveMultiFrame(provider);
image.Save(memStream, Encoder);
memStream.Seek(0, SeekOrigin.Begin);
using Image<TPixel> encoded = Image.Load<TPixel>(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);
}
}

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

124
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";
}
}

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
tests/Images/Input/Icon/24bpp_transp.ico

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb8ea41822350e5f40bac2aef19ec7a4c40561ce6637948b3fa6db7835c1fded
size 3262

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
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

3
tests/Images/Input/Icon/32bpp_transp.ico

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4f94718304fa41041b8cebad7b76762d410b366b46d620f5599b03a2fa7ba00
size 4286

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save