Browse Source

Add Icon Support

- Ico Decoder
- Ico Detector
- Ico Detector UnitTest
- Cur Decoder
- Cur Detector
- Cur Detector UnitTest

Signed-off-by: 舰队的偶像-岛风酱! <frg2089@outlook.com>
pull/2579/head
舰队的偶像-岛风酱! 2 years ago
parent
commit
4832d8f24a
No known key found for this signature in database GPG Key ID: 71F5B3A2B181950C
  1. 7
      ImageSharp.sln
  2. 6
      src/ImageSharp/Configuration.cs
  3. 19
      src/ImageSharp/Formats/Icon/Cur/CurConfigurationModule.cs
  4. 28
      src/ImageSharp/Formats/Icon/Cur/CurConstants.cs
  5. 47
      src/ImageSharp/Formats/Icon/Cur/CurDecoder.cs
  6. 16
      src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs
  7. 37
      src/ImageSharp/Formats/Icon/Cur/CurFormat.cs
  8. 52
      src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs
  9. 16
      src/ImageSharp/Formats/Icon/Cur/CurMetadata.cs
  10. 44
      src/ImageSharp/Formats/Icon/Cur/MetadataExtensions.cs
  11. 19
      src/ImageSharp/Formats/Icon/Ico/IcoConfigurationModule.cs
  12. 38
      src/ImageSharp/Formats/Icon/Ico/IcoConstants.cs
  13. 47
      src/ImageSharp/Formats/Icon/Ico/IcoDecoder.cs
  14. 16
      src/ImageSharp/Formats/Icon/Ico/IcoDecoderCore.cs
  15. 37
      src/ImageSharp/Formats/Icon/Ico/IcoFormat.cs
  16. 50
      src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs
  17. 16
      src/ImageSharp/Formats/Icon/Ico/IcoMetadata.cs
  18. 44
      src/ImageSharp/Formats/Icon/Ico/MetadataExtensions.cs
  19. 43
      src/ImageSharp/Formats/Icon/IconAssert.cs
  20. 136
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  21. 26
      src/ImageSharp/Formats/Icon/IconDir.cs
  22. 28
      src/ImageSharp/Formats/Icon/IconDirEntry.cs
  23. 20
      src/ImageSharp/Formats/Icon/IconFileType.cs
  24. 25
      src/ImageSharp/Formats/Icon/IconFrameCompression.cs
  25. 103
      src/ImageSharp/Formats/Icon/IconFrameMetadata.cs
  26. 33
      src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
  27. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  28. 23
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  29. 23
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  30. 10
      tests/ImageSharp.Tests/TestImages.cs
  31. 3
      tests/Images/Input/Icon/aero_arrow.cur
  32. 3
      tests/Images/Input/Icon/flutter.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

@ -5,6 +5,8 @@ using System.Collections.Concurrent;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Icon.Cur;
using SixLabors.ImageSharp.Formats.Icon.Ico;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.Formats.Png;
@ -222,5 +224,7 @@ public sealed class Configuration
new TgaConfigurationModule(),
new TiffConfigurationModule(),
new WebpConfigurationModule(),
new QoiConfigurationModule());
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
}

19
src/ImageSharp/Formats/Icon/Cur/CurConfigurationModule.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <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)
{
// TODO: CurEncoder
// configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
}

28
src/ImageSharp/Formats/Icon/Cur/CurConstants.cs

@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <summary>
/// Defines constants relating to ICOs
/// </summary>
internal static class CurConstants
{
/// <summary>
/// The list of mimetypes 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 = new[]
{
"application/octet-stream",
};
/// <summary>
/// The list of file extensions that equate to a ico.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "cur" };
public const uint FileHeader = 0x00_02_00_00;
}

47
src/ImageSharp/Formats/Icon/Cur/CurDecoder.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <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);
}
}

16
src/ImageSharp/Formats/Icon/Cur/CurDecoderCore.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
internal sealed class CurDecoderCore : IconDecoderCore
{
public CurDecoderCore(DecoderOptions options)
: base(options)
{
}
protected override IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata) => metadata.GetCurMetadata();
}

37
src/ImageSharp/Formats/Icon/Cur/CurFormat.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <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();
}

52
src/ImageSharp/Formats/Icon/Cur/CurFrameMetadata.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <summary>
/// IcoFrameMetadata
/// </summary>
public class CurFrameMetadata : IconFrameMetadata, IDeepCloneable<CurFrameMetadata>, IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
/// </summary>
public CurFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
/// </summary>
/// <param name="metadata">metadata</param>
public CurFrameMetadata(IconFrameMetadata metadata)
: base(metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class.
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="colorCount">colorCount</param>
/// <param name="field1">field1</param>
/// <param name="field2">field2</param>
public CurFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
: base(width, height, colorCount, field1, field2)
{
}
/// <summary>
/// Gets or sets Specifies the horizontal coordinates of the hotspot in number of pixels from the left.
/// </summary>
public ushort HotspotX { get => this.Field1; set => this.Field1 = value; }
/// <summary>
/// Gets or sets Specifies the vertical coordinates of the hotspot in number of pixels from the top.
/// </summary>
public ushort HotspotY { get => this.Field2; set => this.Field2 = value; }
/// <inheritdoc/>
public override CurFrameMetadata DeepClone() => new(this);
}

16
src/ImageSharp/Formats/Icon/Cur/CurMetadata.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <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();
}

44
src/ImageSharp/Formats/Icon/Cur/MetadataExtensions.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Icon.Cur;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static 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);
}

19
src/ImageSharp/Formats/Icon/Ico/IcoConfigurationModule.cs

@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <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)
{
// TODO: IcoEncoder
// configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
}
}

38
src/ImageSharp/Formats/Icon/Ico/IcoConstants.cs

@ -0,0 +1,38 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <summary>
/// Defines constants relating to ICOs
/// </summary>
internal static class IcoConstants
{
/// <summary>
/// The list of mimetypes 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 = new[]
{
// IANA-registered
"image/vnd.microsoft.icon",
// ICO & CUR types used by Windows
"image/x-icon",
// Erroneous types but have been used
"image/ico",
"image/icon",
"text/ico",
"application/ico",
};
/// <summary>
/// The list of file extensions that equate to a ico.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "ico" };
public const uint FileHeader = 0x00_01_00_00;
}

47
src/ImageSharp/Formats/Icon/Ico/IcoDecoder.cs

@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <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);
}
}

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

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
internal sealed class IcoDecoderCore : IconDecoderCore
{
public IcoDecoderCore(DecoderOptions options)
: base(options)
{
}
protected override IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata) => metadata.GetIcoMetadata();
}

37
src/ImageSharp/Formats/Icon/Ico/IcoFormat.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <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();
}

50
src/ImageSharp/Formats/Icon/Ico/IcoFrameMetadata.cs

@ -0,0 +1,50 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <summary>
/// IcoFrameMetadata
/// </summary>
public class IcoFrameMetadata : IconFrameMetadata, IDeepCloneable<IcoFrameMetadata>, IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
/// </summary>
public IcoFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
/// </summary>
/// <param name="metadata">metadata</param>
public IcoFrameMetadata(IconFrameMetadata metadata)
: base(metadata)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class.
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="colorCount">colorCount</param>
/// <param name="field1">field1</param>
/// <param name="field2">field2</param>
public IcoFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
: base(width, height, colorCount, field1, field2)
{
}
/// <summary>
/// Gets or sets Specifies bits per pixel.
/// </summary>
/// <remarks>
/// It may used by Encoder.
/// </remarks>
public ushort BitCount { get => this.Field2; set => this.Field2 = value; }
/// <inheritdoc/>
public override IcoFrameMetadata DeepClone() => new(this);
}

16
src/ImageSharp/Formats/Icon/Ico/IcoMetadata.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <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();
}

44
src/ImageSharp/Formats/Icon/Ico/MetadataExtensions.cs

@ -0,0 +1,44 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Icon.Ico;
/// <summary>
/// Extension methods for the <see cref="ImageMetadata"/> type.
/// </summary>
public static 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);
}

43
src/ImageSharp/Formats/Icon/IconAssert.cs

@ -0,0 +1,43 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon;
internal class IconAssert
{
internal static void CanSeek(Stream stream)
{
if (!stream.CanSeek)
{
throw new NotSupportedException("This stream cannot support seek");
}
}
internal static int EndOfStream(int v, int length)
{
if (v != length)
{
throw new EndOfStreamException();
}
return v;
}
internal static long EndOfStream(long v, long length)
{
if (v != length)
{
throw new EndOfStreamException();
}
return v;
}
internal static void NotSquare(in Size size)
{
if (size.Width != size.Height)
{
throw new FormatException("This image is not square.");
}
}
}

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

@ -0,0 +1,136 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Icon;
internal abstract class IconDecoderCore : IImageDecoderInternals
{
private IconDir fileHeader;
public IconDecoderCore(DecoderOptions options) => this.Options = options;
public DecoderOptions Options { get; }
public Size Dimensions { get; private set; }
protected IconDir FileHeader { get => this.fileHeader; private set => this.fileHeader = value; }
protected IconDirEntry[] Entries { get; private set; } = Array.Empty<IconDirEntry>();
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[Png.PngConstants.HeaderBytes.Length];
List<(Image<TPixel> Image, bool IsPng, int Index)> frames = new(this.Entries.Length);
for (int i = 0; i < this.Entries.Length; i++)
{
_ = IconAssert.EndOfStream(stream.Seek(basePosition + this.Entries[i].ImageOffset, SeekOrigin.Begin), basePosition + this.Entries[i].ImageOffset);
_ = IconAssert.EndOfStream(stream.Read(flag), Png.PngConstants.HeaderBytes.Length);
_ = stream.Seek(-Png.PngConstants.HeaderBytes.Length, SeekOrigin.Current);
bool isPng = flag.SequenceEqual(Png.PngConstants.HeaderBytes);
Image<TPixel> img = this.GetDecoder(isPng).Decode<TPixel>(stream, cancellationToken);
IconAssert.NotSquare(img.Size);
frames.Add((img, isPng, i));
if (isPng && img.Size.Width > this.Dimensions.Width)
{
this.Dimensions = img.Size;
}
}
ImageMetadata metadata = new();
return new(this.Options.Configuration, metadata, frames.Select(i =>
{
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = i.Image.Frames.RootFrameUnsafe;
for (int h = 0; h < source.Height; h++)
{
source.PixelBuffer.DangerousGetRowSpan(h).CopyTo(target.PixelBuffer.DangerousGetRowSpan(h));
}
if (i.IsPng)
{
target.Metadata.UnsafeSetFormatMetadata(Png.PngFormat.Instance, i.Image.Metadata.GetPngMetadata());
}
else
{
target.Metadata.UnsafeSetFormatMetadata(Bmp.BmpFormat.Instance, i.Image.Metadata.GetBmpMetadata());
}
this.GetFrameMetadata(target.Metadata).FromIconDirEntry(this.Entries[i.Index]);
i.Image.Dispose();
return target;
}).ToArray());
}
public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
this.ReadHeader(stream);
ImageMetadata metadata = new();
ImageFrameMetadata[] frames = new ImageFrameMetadata[this.FileHeader.Count];
for (int i = 0; i < frames.Length; i++)
{
frames[i] = new();
IconFrameMetadata icoFrameMetadata = this.GetFrameMetadata(frames[i]);
icoFrameMetadata.FromIconDirEntry(this.Entries[i]);
}
return new(new(32), new(0), metadata, frames);
}
protected abstract IconFrameMetadata GetFrameMetadata(ImageFrameMetadata metadata);
protected void ReadHeader(Stream stream)
{
_ = Read(stream, out this.fileHeader, IconDir.Size);
this.Entries = new IconDirEntry[this.FileHeader.Count];
for (int i = 0; i < this.Entries.Length; i++)
{
_ = Read(stream, out this.Entries[i], IconDirEntry.Size);
}
this.Dimensions = new(
this.Entries.Max(i => i.Width),
this.Entries.Max(i => i.Height));
}
private static int Read<T>(Stream stream, out T data, int size)
where T : unmanaged
{
Span<byte> buffer = stackalloc byte[size];
_ = IconAssert.EndOfStream(stream.Read(buffer), size);
data = MemoryMarshal.Cast<byte, T>(buffer)[0];
return size;
}
private IImageDecoderInternals GetDecoder(bool isPng)
{
if (isPng)
{
return new Png.PngDecoderCore(this.Options);
}
else
{
return new Bmp.BmpDecoderCore(new()
{
ProcessedAlphaMask = true,
SkipFileHeader = true,
IsDoubleHeight = true,
});
}
}
}

26
src/ImageSharp/Formats/Icon/IconDir.cs

@ -0,0 +1,26 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Icon;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
internal struct IconDir
{
public const int Size = 3 * sizeof(ushort);
public ushort Reserved;
public IconFileType Type;
public ushort Count;
public IconDir(IconFileType type)
: this(type, 0)
=> this.Type = type;
public IconDir(IconFileType type, ushort count)
{
this.Reserved = 0;
this.Type = type;
this.Count = count;
}
}

28
src/ImageSharp/Formats/Icon/IconDirEntry.cs

@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Icon;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
internal struct IconDirEntry
{
public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint));
public byte Width;
public byte Height;
public byte ColorCount;
public byte Reserved;
public ushort Planes;
public ushort BitCount;
public uint BytesInRes;
public uint ImageOffset;
}

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

25
src/ImageSharp/Formats/Icon/IconFrameCompression.cs

@ -0,0 +1,25 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// IconFrameCompression
/// </summary>
public enum IconFrameCompression
{
/// <summary>
/// Unknown
/// </summary>
Unknown,
/// <summary>
/// Bmp
/// </summary>
Bmp,
/// <summary>
/// Png
/// </summary>
Png
}

103
src/ImageSharp/Formats/Icon/IconFrameMetadata.cs

@ -0,0 +1,103 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// IconFrameMetadata
/// </summary>
public abstract class IconFrameMetadata : IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="IconFrameMetadata"/> class.
/// </summary>
protected IconFrameMetadata()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IconFrameMetadata"/> class.
/// </summary>
/// <param name="width">width</param>
/// <param name="height">height</param>
/// <param name="colorCount">colorCount</param>
/// <param name="field1">field1</param>
/// <param name="field2">field2</param>
protected IconFrameMetadata(byte width, byte height, byte colorCount, ushort field1, ushort field2)
{
this.EncodingWidth = width;
this.EncodingHeight = height;
this.ColorCount = colorCount;
this.Field1 = field1;
this.Field2 = field2;
}
/// <inheritdoc cref="IconFrameMetadata()"/>
protected IconFrameMetadata(IconFrameMetadata metadata)
{
this.EncodingWidth = metadata.EncodingWidth;
this.EncodingHeight = metadata.EncodingHeight;
this.ColorCount = metadata.ColorCount;
this.Field1 = metadata.Field1;
this.Field2 = metadata.Field2;
}
/// <summary>
/// Gets or sets icoFrameCompression.
/// </summary>
public IconFrameCompression Compression { get; protected set; }
/// <summary>
/// Gets or sets ColorCount field. <br />
/// Specifies number of colors in the color palette. Should be 0 if the image does not use a color palette.
/// </summary>
// TODO: BmpMetadata does not supported palette yet.
public byte ColorCount { get; set; }
/// <summary>
/// Gets or sets Planes field. <br />
/// 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>
protected ushort Field1 { get; set; }
/// <summary>
/// Gets or sets BitCount field. <br />
/// 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>
protected ushort Field2 { get; set; }
/// <summary>
/// Gets or sets Height field. <br />
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
/// </summary>
public byte EncodingHeight { get; set; }
/// <summary>
/// Gets or sets Width field. <br />
/// Specifies image width in pixels. Can be any number between 0 and 255. Value 0 means image width is 256 pixels.
/// </summary>
public byte EncodingWidth { get; set; }
/// <inheritdoc/>
public abstract IDeepCloneable DeepClone();
internal void FromIconDirEntry(in IconDirEntry metadata)
{
this.EncodingWidth = metadata.Width;
this.EncodingHeight = metadata.Height;
this.ColorCount = metadata.ColorCount;
this.Field1 = metadata.Planes;
this.Field2 = metadata.BitCount;
}
internal IconDirEntry ToIconDirEntry() => new()
{
Width = this.EncodingWidth,
Height = this.EncodingHeight,
ColorCount = this.ColorCount,
Planes = this.Field1,
BitCount = this.Field2,
};
}

33
src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs

@ -0,0 +1,33 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// Detects ico file headers.
/// </summary>
public class IconImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize { get; } = 4;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
switch (MemoryMarshal.Cast<byte, uint>(header)[0])
{
case Ico.IcoConstants.FileHeader:
format = Ico.IcoFormat.Instance;
return true;
case Cur.CurConstants.FileHeader:
format = Cur.CurFormat.Instance;
return true;
default:
format = default;
return false;
}
}
}

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

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

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon.Cur;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Tests.TestImages.Cur;
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
[Trait("Format", "Cur")]
[ValidateDisposedMemoryAllocations]
public class CurDecoderTests
{
[Theory]
[WithFile(WindowsMouse, PixelTypes.Rgba32)]
public void CurDecoder_Decode(TestImageProvider<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage(CurDecoder.Instance);
image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder());
}
}

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

@ -0,0 +1,23 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon.Ico;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Tests.TestImages.Ico;
namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
[Trait("Format", "Icon")]
[ValidateDisposedMemoryAllocations]
public class IcoDecoderTests
{
[Theory]
[WithFile(Flutter, PixelTypes.Rgba32)]
public void IcoDecoder_Decode(TestImageProvider<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
image.DebugSave(provider, extension: "tiff", encoder: new TiffEncoder());
}
}

10
tests/ImageSharp.Tests/TestImages.cs

@ -1121,4 +1121,14 @@ 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 static class Cur
{
public const string WindowsMouse = "Icon/aero_arrow.cur";
}
}

3
tests/Images/Input/Icon/aero_arrow.cur

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

3
tests/Images/Input/Icon/flutter.ico

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c098d3fc85cacff98b8e69811b48e9f0d852fcee278132d794411d978869cbf8
size 33772
Loading…
Cancel
Save