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