From aac7d3ab8a788e35b2cc6d468673ff23e0162f59 Mon Sep 17 00:00:00 2001 From: Betta_Fish Date: Sat, 1 Feb 2025 02:31:36 +0800 Subject: [PATCH 01/20] Add Header --- src/ImageSharp/Formats/Ani/AniDecoder.cs | 35 ++++++++++++++++++++ src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 35 ++++++++++++++++++++ src/ImageSharp/Formats/Ani/AniHeader.cs | 34 +++++++++++++++++++ src/ImageSharp/Formats/Ani/AniMetadata.cs | 23 +++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/ImageSharp/Formats/Ani/AniDecoder.cs create mode 100644 src/ImageSharp/Formats/Ani/AniDecoderCore.cs create mode 100644 src/ImageSharp/Formats/Ani/AniHeader.cs create mode 100644 src/ImageSharp/Formats/Ani/AniMetadata.cs diff --git a/src/ImageSharp/Formats/Ani/AniDecoder.cs b/src/ImageSharp/Formats/Ani/AniDecoder.cs new file mode 100644 index 0000000000..9794867feb --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniDecoder.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal sealed class AniDecoder : ImageDecoder +{ + private AniDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static AniDecoder 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 AniDecoderCore(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 AniDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } +} diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs new file mode 100644 index 0000000000..c60036c465 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal class AniDecoderCore : ImageDecoderCore +{ + public AniDecoderCore(DecoderOptions options) + : base(options) + { + } + + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + this.ReadHeader(stream); + throw new NotImplementedException(); + } + + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + private void ReadHeader(Stream stream) + { + // Skip the identifier + stream.Skip(12); + Span buffer = stackalloc byte[AniHeader.Size]; + _ = stream.Read(buffer); + AniHeader header = AniHeader.Parse(buffer); + } +} diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs new file mode 100644 index 0000000000..8f985c4c6e --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Ani; + +[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] +internal readonly struct AniHeader +{ + public const int Size = 36; + + public ushort Frames { get; } + + public ushort Steps { get; } + + public ushort Width { get; } + + public ushort Height { get; } + + public ushort BitCount { get; } + + public ushort Planes { get; } + + public ushort DisplayRate { get; } + + public ushort Flags { get; } + + public static AniHeader Parse(in ReadOnlySpan data) + => MemoryMarshal.Cast(data)[0]; + + public readonly unsafe void WriteTo(in Stream stream) + => stream.Write(MemoryMarshal.Cast([this])); +} diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs new file mode 100644 index 0000000000..ce862b5eef --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal class AniMetadata : IFormatMetadata +{ + + public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + + public void AfterImageApply(Image destination) + where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + + public IDeepCloneable DeepClone() => throw new NotImplementedException(); + + public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + + public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + + AniMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); +} From 9055bed91b5a5a87725ff98b8d237832189ee368 Mon Sep 17 00:00:00 2001 From: Betta_Fish Date: Sun, 2 Feb 2025 18:09:02 +0800 Subject: [PATCH 02/20] Add Unit Test --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 17 +++++++++++- src/ImageSharp/Formats/Ani/AniHeader.cs | 27 ++++++++++++------- .../Formats/Ani/AniDecoderTests.cs | 23 ++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 6 +++++ tests/Images/Input/Ani/Work.ani | 3 +++ 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs create mode 100644 tests/Images/Input/Ani/Work.ani diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index c60036c465..45fb040a29 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -16,6 +16,21 @@ public AniDecoderCore(DecoderOptions options) protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadHeader(stream); + Span buffer = stackalloc byte[4]; + _ = stream.Read(buffer); + uint type = BitConverter.ToUInt32(buffer); + switch (type) + { + case 0x73_65_71_20: // seq + break; + case 0x72_61_74_65: // rate + break; + case 0x4C_49_53_54: // list + break; + default: + break; + } + throw new NotImplementedException(); } @@ -28,7 +43,7 @@ private void ReadHeader(Stream stream) { // Skip the identifier stream.Skip(12); - Span buffer = stackalloc byte[AniHeader.Size]; + Span buffer = stackalloc byte[36]; _ = stream.Read(buffer); AniHeader header = AniHeader.Parse(buffer); } diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 8f985c4c6e..514ce710d3 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -5,26 +5,26 @@ namespace SixLabors.ImageSharp.Formats.Ani; -[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] +[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 36)] internal readonly struct AniHeader { - public const int Size = 36; + public uint Size { get; } - public ushort Frames { get; } + public uint Frames { get; } - public ushort Steps { get; } + public uint Steps { get; } - public ushort Width { get; } + public uint Width { get; } - public ushort Height { get; } + public uint Height { get; } - public ushort BitCount { get; } + public uint BitCount { get; } - public ushort Planes { get; } + public uint Planes { get; } - public ushort DisplayRate { get; } + public uint DisplayRate { get; } - public ushort Flags { get; } + public AniHeaderFlags Flags { get; } public static AniHeader Parse(in ReadOnlySpan data) => MemoryMarshal.Cast(data)[0]; @@ -32,3 +32,10 @@ public static AniHeader Parse(in ReadOnlySpan data) public readonly unsafe void WriteTo(in Stream stream) => stream.Write(MemoryMarshal.Cast([this])); } + +[Flags] +public enum AniHeaderFlags : uint +{ + IsIcon = 1, + ContainsSeq = 2 +} diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs new file mode 100644 index 0000000000..f79570f22c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Ani; +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.Ani; + +namespace SixLabors.ImageSharp.Tests.Formats.Ani; + +[Trait("format", "Ani")] +[ValidateDisposedMemoryAllocations] +public class AniDecoderTests +{ + [Theory] + [WithFile(Work, PixelTypes.Rgba32)] + public void CurDecoder_Decode(TestImageProvider provider) + { + using Image image = provider.GetImage(AniDecoder.Instance); + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 4130474b58..d928b82b3d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1254,4 +1254,10 @@ public static class Cur public const string CurReal = "Icon/cur_real.cur"; public const string CurFake = "Icon/cur_fake.ico"; } + + public static class Ani + { + public const string Work = "Ani/Work.ani"; + } + } diff --git a/tests/Images/Input/Ani/Work.ani b/tests/Images/Input/Ani/Work.ani new file mode 100644 index 0000000000..d576244bbd --- /dev/null +++ b/tests/Images/Input/Ani/Work.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:740353739d3763addddd383614d125918781b8879f7c1ad3c770162a3e143a33 +size 1150338 From 1a51a5c23ba24dde8a2599cb17f32b960a171848 Mon Sep 17 00:00:00 2001 From: Betta_Fish Date: Tue, 4 Feb 2025 15:02:38 +0800 Subject: [PATCH 03/20] Add AniChunk --- src/ImageSharp/Formats/Ani/AniChunk.cs | 38 ++++ src/ImageSharp/Formats/Ani/AniChunkType.cs | 11 + src/ImageSharp/Formats/Ani/AniConstants.cs | 21 ++ src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 196 ++++++++++++++++-- src/ImageSharp/Formats/Ani/AniFormat.cs | 34 +++ src/ImageSharp/Formats/Ani/AniMetadata.cs | 20 +- .../Formats/Bmp/BmpRenderingIntent.cs | 2 +- .../_Generated/ImageMetadataExtensions.cs | 21 ++ 8 files changed, 320 insertions(+), 23 deletions(-) create mode 100644 src/ImageSharp/Formats/Ani/AniChunk.cs create mode 100644 src/ImageSharp/Formats/Ani/AniChunkType.cs create mode 100644 src/ImageSharp/Formats/Ani/AniConstants.cs create mode 100644 src/ImageSharp/Formats/Ani/AniFormat.cs diff --git a/src/ImageSharp/Formats/Ani/AniChunk.cs b/src/ImageSharp/Formats/Ani/AniChunk.cs new file mode 100644 index 0000000000..d644749223 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniChunk.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#nullable disable +using System.Buffers; +using SixLabors.ImageSharp.Formats.Png; + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal readonly struct AniChunk +{ + public AniChunk(int length, AniChunkType type, IMemoryOwner data = null) + { + this.Length = length; + this.Type = type; + this.Data = data; + } + + /// + /// Gets the length. + /// An unsigned integer giving the number of bytes in the chunk's + /// data field. The length counts only the data field, not itself, + /// the chunk type code, or the CRC. Zero is a valid length + /// + public int Length { get; } + + /// + /// Gets the chunk type. + /// The value is the equal to the UInt32BigEndian encoding of its 4 ASCII characters. + /// + public AniChunkType Type { get; } + + /// + /// Gets the data bytes appropriate to the chunk type, if any. + /// This field can be of zero length or null. + /// + public IMemoryOwner Data { get; } +} diff --git a/src/ImageSharp/Formats/Ani/AniChunkType.cs b/src/ImageSharp/Formats/Ani/AniChunkType.cs new file mode 100644 index 0000000000..00419cae5b --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniChunkType.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal enum AniChunkType : uint +{ + Seq = 0x73_65_71_20, + Rate = 0x72_61_74_65, + List = 0x4C_49_53_54 +} diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs new file mode 100644 index 0000000000..7d59098c40 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +internal static class AniConstants +{ + /// + /// The list of mime types that equate to an ani. + /// + /// + /// See + /// + public static readonly IEnumerable MimeTypes = []; + + /// + /// The list of file extensions that equate to an ani. + /// + public static readonly IEnumerable FileExtensions = ["ani"]; + +} diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 45fb040a29..f6b0c98a1c 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -1,50 +1,206 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory.Internals; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; namespace SixLabors.ImageSharp.Formats.Ani; internal class AniDecoderCore : ImageDecoderCore { + /// + /// The general decoder options. + /// + private readonly Configuration configuration; + + /// + /// The stream to decode from. + /// + private BufferedReadStream currentStream = null!; + + private AniHeader header; + public AniDecoderCore(DecoderOptions options) - : base(options) - { - } + : base(options) => this.configuration = options.Configuration; protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { - this.ReadHeader(stream); - Span buffer = stackalloc byte[4]; - _ = stream.Read(buffer); - uint type = BitConverter.ToUInt32(buffer); - switch (type) + this.currentStream = stream; + this.ReadHeader(); + ImageMetadata metadata = new(); + AniMetadata aniMetadata = metadata.GetAniMetadata(); + Image? image = null; + + Span buffer = stackalloc byte[20]; + + try { - case 0x73_65_71_20: // seq - break; - case 0x72_61_74_65: // rate - break; - case 0x4C_49_53_54: // list - break; - default: - break; + while (this.TryReadChunk(buffer, out AniChunk chunk)) + { + try + { + switch (chunk.Type) + { + case AniChunkType.Seq: + + break; + case AniChunkType.Rate: + + break; + case AniChunkType.List: + + break; + default: + break; + } + } + finally + { + chunk.Data?.Dispose(); + } + } } + catch + { + image?.Dispose(); + throw; + } + throw new NotImplementedException(); } + private void ReadSeq() + { + + } + + private bool TryReadChunk(Span buffer, out AniChunk chunk) + { + if (!this.TryReadChunkLength(buffer, out int length)) + { + // IEND + chunk = default; + return false; + } + + while (length < 0) + { + // Not a valid chunk so try again until we reach a known chunk. + if (!this.TryReadChunkLength(buffer, out length)) + { + // IEND + chunk = default; + return false; + } + } + + AniChunkType type = this.ReadChunkType(buffer); + + // A chunk might report a length that exceeds the length of the stream. + // Take the minimum of the two values to ensure we don't read past the end of the stream. + long position = this.currentStream.Position; + chunk = new AniChunk( + length: (int)Math.Min(length, this.currentStream.Length - position), + type: type, + data: this.ReadChunkData(length)); + + return true; + } + + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { throw new NotImplementedException(); } - private void ReadHeader(Stream stream) + private void ReadHeader() { // Skip the identifier - stream.Skip(12); + this.currentStream.Skip(12); Span buffer = stackalloc byte[36]; - _ = stream.Read(buffer); - AniHeader header = AniHeader.Parse(buffer); + _ = this.currentStream.Read(buffer); + this.header = AniHeader.Parse(buffer); + } + + private void ReadSeq(Stream stream) + { + Span buffer = stackalloc byte[4]; + int length = BinaryPrimitives.ReadInt32BigEndian(buffer); + } + + /// + /// Attempts to read the length of the next chunk. + /// + /// Temporary buffer. + /// The result length. If the return type is this parameter is passed uninitialized. + /// + /// Whether the length was read. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private bool TryReadChunkLength(Span buffer, out int result) + { + if (this.currentStream.Read(buffer, 0, 4) == 4) + { + result = BinaryPrimitives.ReadInt32BigEndian(buffer); + + return true; + } + + result = 0; + return false; + } + + /// + /// Identifies the chunk type from the chunk. + /// + /// Temporary buffer. + /// + /// Thrown if the input stream is not valid. + /// + [MethodImpl(InliningOptions.ShortMethod)] + private AniChunkType ReadChunkType(Span buffer) + { + if (this.currentStream.Read(buffer, 0, 4) == 4) + { + return (AniChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + + PngThrowHelper.ThrowInvalidChunkType(); + + // The IDE cannot detect the throw here. + return default; + } + + /// + /// Reads the chunk data from the stream. + /// + /// The length of the chunk data to read. + [MethodImpl(InliningOptions.ShortMethod)] + private IMemoryOwner ReadChunkData(int length) + { + if (length == 0) + { + return new BasicArrayBuffer([]); + } + + // We rent the buffer here to return it afterwards in Decode() + // We don't want to throw a degenerated memory exception here as we want to allow partial decoding + // so limit the length. + length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); + IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); + + this.currentStream.Read(buffer.GetSpan(), 0, length); + + return buffer; + } + } diff --git a/src/ImageSharp/Formats/Ani/AniFormat.cs b/src/ImageSharp/Formats/Ani/AniFormat.cs new file mode 100644 index 0000000000..16f707390a --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniFormat.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + + +using SixLabors.ImageSharp.Formats.Ico; + +namespace SixLabors.ImageSharp.Formats.Ani; + + +/// +/// Registers the image encoders, decoders and mime type detectors for the bmp format. +/// +public sealed class AniFormat : IImageFormat +{ + /// + /// Gets the shared instance. + /// + public static AniFormat Instance { get; } = new(); + + /// + public AniMetadata CreateDefaultFormatMetadata() => throw new NotImplementedException(); + + /// + public string Name => "ANI"; + + /// + public string DefaultMimeType { get; } + + /// + public IEnumerable MimeTypes { get; } + + /// + public IEnumerable FileExtensions { get; } +} diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index ce862b5eef..d373a7d9a5 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -1,23 +1,39 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; -internal class AniMetadata : IFormatMetadata +/// +/// Provides Ani specific metadata information for the image. +/// +public class AniMetadata : IFormatMetadata { - + /// + /// Initializes a new instance of the class. + /// + public AniMetadata() + { + } + + /// public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + /// public void AfterImageApply(Image destination) where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + /// public IDeepCloneable DeepClone() => throw new NotImplementedException(); + /// public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + /// public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + /// AniMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs index 87a1f19cc7..32dbb4a51e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -17,7 +17,7 @@ internal enum BmpRenderingIntent /// /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. /// - LCS_GM_BUSINESS = 1, + LCS_GM_BUSINESS = 1, /// /// Maintains colorimetric match. Used for graphic designs and named colors. diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index e35d00ed39..d3542de7af 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.Ani; namespace SixLabors.ImageSharp; @@ -102,6 +103,26 @@ public static class ImageMetadataExtensions /// The new public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata From e58010abea2ce8c0290954c86f8a62c5553a29a8 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 5 Mar 2025 15:49:52 +0800 Subject: [PATCH 04/20] finish decoder --- src/ImageSharp/Formats/Ani/AniChunk.cs | 38 -- src/ImageSharp/Formats/Ani/AniChunkType.cs | 65 ++- .../Formats/Ani/AniConfigurationModule.cs | 18 + src/ImageSharp/Formats/Ani/AniConstants.cs | 25 +- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 427 +++++++++++++----- src/ImageSharp/Formats/Ani/AniFormat.cs | 19 +- .../Formats/Ani/AniFrameMetadata.cs | 70 +++ src/ImageSharp/Formats/Ani/AniHeader.cs | 18 +- .../Formats/Ani/AniImageFormatDetector.cs | 30 ++ src/ImageSharp/Formats/Ani/AniMetadata.cs | 47 +- .../Formats/Bmp/BmpRenderingIntent.cs | 2 +- src/ImageSharp/Formats/Icon/IconDir.cs | 16 +- src/ImageSharp/Formats/Icon/IconDirEntry.cs | 9 +- .../Formats/Icon/IconEncoderCore.cs | 20 +- src/ImageSharp/Formats/Icon/IconFileType.cs | 2 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 6 +- .../Formats/Webp/Chunks/WebpVp8X.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 2 +- src/ImageSharp/Formats/Webp/RiffHelper.cs | 154 +++---- src/ImageSharp/Formats/Webp/WebpConstants.cs | 29 +- .../Formats/Webp/WebpImageFormatDetector.cs | 24 +- .../_Generated/ImageMetadataExtensions.cs | 20 + src/ImageSharp/IO/BufferedReadStream.cs | 57 ++- .../Formats/Ani/AniDecoderTests.cs | 7 +- tests/ImageSharp.Tests/TestImages.cs | 3 +- tests/Images/Input/Ani/Help.ani | 3 + tests/Images/Input/Ani/aero_busy.ani | 3 + 28 files changed, 784 insertions(+), 334 deletions(-) delete mode 100644 src/ImageSharp/Formats/Ani/AniChunk.cs create mode 100644 src/ImageSharp/Formats/Ani/AniConfigurationModule.cs create mode 100644 src/ImageSharp/Formats/Ani/AniFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs create mode 100644 tests/Images/Input/Ani/Help.ani create mode 100644 tests/Images/Input/Ani/aero_busy.ani diff --git a/src/ImageSharp/Formats/Ani/AniChunk.cs b/src/ImageSharp/Formats/Ani/AniChunk.cs deleted file mode 100644 index d644749223..0000000000 --- a/src/ImageSharp/Formats/Ani/AniChunk.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#nullable disable -using System.Buffers; -using SixLabors.ImageSharp.Formats.Png; - -namespace SixLabors.ImageSharp.Formats.Ani; - -internal readonly struct AniChunk -{ - public AniChunk(int length, AniChunkType type, IMemoryOwner data = null) - { - this.Length = length; - this.Type = type; - this.Data = data; - } - - /// - /// Gets the length. - /// An unsigned integer giving the number of bytes in the chunk's - /// data field. The length counts only the data field, not itself, - /// the chunk type code, or the CRC. Zero is a valid length - /// - public int Length { get; } - - /// - /// Gets the chunk type. - /// The value is the equal to the UInt32BigEndian encoding of its 4 ASCII characters. - /// - public AniChunkType Type { get; } - - /// - /// Gets the data bytes appropriate to the chunk type, if any. - /// This field can be of zero length or null. - /// - public IMemoryOwner Data { get; } -} diff --git a/src/ImageSharp/Formats/Ani/AniChunkType.cs b/src/ImageSharp/Formats/Ani/AniChunkType.cs index 00419cae5b..35eba01140 100644 --- a/src/ImageSharp/Formats/Ani/AniChunkType.cs +++ b/src/ImageSharp/Formats/Ani/AniChunkType.cs @@ -5,7 +5,66 @@ namespace SixLabors.ImageSharp.Formats.Ani; internal enum AniChunkType : uint { - Seq = 0x73_65_71_20, - Rate = 0x72_61_74_65, - List = 0x4C_49_53_54 + /// + /// "anih" + /// + AniH = 0x68_69_6E_61, + + /// + /// "seq " + /// + Seq = 0x20_71_65_73, + + /// + /// "rate" + /// + Rate = 0x65_74_61_72, + + /// + /// "LIST" + /// + List = 0x54_53_49_4C +} + +/// +/// ListType +/// +internal enum AniListType : uint +{ + /// + /// "INFO" (ListType) + /// + Info = 0x4F_46_4E_49, + + /// + /// "fram" + /// + Fram = 0x6D_61_72_66 +} + +/// +/// in "INFO" +/// +internal enum AniListInfoType : uint +{ + /// + /// "INAM" + /// + INam = 0x4D_41_4E_49, + + /// + /// "IART" + /// + IArt = 0x54_52_41_49 +} + +/// +/// in "Fram" +/// +internal enum AniListFrameType : uint +{ + /// + /// "icon" + /// + Icon = 0x6E_6F_63_69 } diff --git a/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs new file mode 100644 index 0000000000..6cf3a55f10 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Registers the image encoders, decoders and mime type detectors for the Ico format. +/// +public sealed class AniConfigurationModule : IImageFormatConfigurationModule +{ + /// + public void Configure(Configuration configuration) + { + // configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder()); + configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector()); + } +} diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs index 7d59098c40..0b40efa31f 100644 --- a/src/ImageSharp/Formats/Ani/AniConstants.cs +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -8,14 +8,31 @@ internal static class AniConstants /// /// The list of mime types that equate to an ani. /// - /// - /// See - /// - public static readonly IEnumerable MimeTypes = []; + public static readonly IEnumerable MimeTypes = ["application/x-navi-animation"]; /// /// The list of file extensions that equate to an ani. /// public static readonly IEnumerable FileExtensions = ["ani"]; + /// + /// Gets the header bytes identifying an ani. + /// + public static ReadOnlySpan AniFormTypeFourCc => "ACON"u8; + + /// + /// Gets the header bytes identifying an ani. + /// + public const uint AniFourCc = 0x41_43_4F_4E; + + public static class ChunkFourCcs + { + public static ReadOnlySpan AniHeader => "anih"u8; + + public static ReadOnlySpan Seq => "seq "u8; + + public static ReadOnlySpan Rate => "rate"u8; + + public static ReadOnlySpan Icon => "icon"u8; + } } diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index f6b0c98a1c..ab79a14e0a 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -2,23 +2,36 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Buffers.Binary; +using System.Collections; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Icon; -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; -internal class AniDecoderCore : ImageDecoderCore +internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options) { + private enum ListIconChunkType + { + Ico = 1, + Cur = 2, + Bmp = 3 + } + /// /// The general decoder options. /// - private readonly Configuration configuration; + private readonly Configuration configuration = options.Configuration; /// /// The stream to decode from. @@ -27,157 +40,354 @@ internal class AniDecoderCore : ImageDecoderCore private AniHeader header; - public AniDecoderCore(DecoderOptions options) - : base(options) => this.configuration = options.Configuration; - protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.currentStream = stream; - this.ReadHeader(); + + Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); + long dataSize = riffHeader.Size; + long dataStartPosition = this.currentStream.Position; + ImageMetadata metadata = new(); - AniMetadata aniMetadata = metadata.GetAniMetadata(); - Image? image = null; + AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + + List<(ListIconChunkType Type, Image Image)> frames = []; + this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk); - Span buffer = stackalloc byte[20]; + List> list = []; - try + foreach (int i in sequence) { - while (this.TryReadChunk(buffer, out AniChunk chunk)) + (ListIconChunkType type, Image? img) = frames[i]; + byte? encodingWidth = null; + byte? encodingHeight = null; + bool isRootFrame = true; + list.AddRange(img.Frames.Select(source => { - try + ImageFrame target = new(this.Options.Configuration, this.Dimensions); + for (int y = 0; y < source.Height; y++) { - switch (chunk.Type) - { - case AniChunkType.Seq: + source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + } - break; - case AniChunkType.Rate: + switch (type) + { + case ListIconChunkType.Ico: + IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); + target.Metadata.SetFormatMetadata(IcoFormat.Instance, icoFrameMetadata); + if (isRootFrame) + { + encodingWidth ??= icoFrameMetadata.EncodingWidth; + encodingHeight ??= icoFrameMetadata.EncodingHeight; + } + + break; + case ListIconChunkType.Cur: + CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); + target.Metadata.SetFormatMetadata(CurFormat.Instance, curFrameMetadata); + if (isRootFrame) + { + encodingWidth ??= curFrameMetadata.EncodingWidth; + encodingHeight ??= curFrameMetadata.EncodingHeight; + } + + break; + case ListIconChunkType.Bmp: + if (isRootFrame) + { + encodingWidth = Narrow(source.Width); + encodingHeight = Narrow(source.Height); + } + + break; + default: + break; + } - break; - case AniChunkType.List: + isRootFrame = false; - break; - default: - break; + return target; + })); + + ImageFrameMetadata rootFrameMetadata = img.Frames.RootFrame.Metadata; + AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); + aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameCount = img.Frames.Count; + aniFrameMetadata.EncodingWidth = encodingWidth; + aniFrameMetadata.EncodingHeight = encodingHeight; + aniFrameMetadata.SubImageMetadata = img.Metadata; + aniMetadata.IconFrames.Add(rootFrameMetadata); + } + + foreach ((ListIconChunkType _, Image img) in frames) + { + img.Dispose(); + } + + Image image = new(this.Options.Configuration, metadata, list); + + return image; + + void DecodeFrameChunk() + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) + { + continue; + } + + long endPosition = this.currentStream.Position + chunk.Size; + Image? frame = null; + ListIconChunkType type = default; + if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) + { + if (this.currentStream.TryReadUnmanaged(out IconDir dir)) + { + this.currentStream.Position -= Unsafe.SizeOf(); + + switch (dir.Type) + { + case IconFileType.CUR: + frame = CurDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Cur; + break; + case IconFileType.ICO: + frame = IcoDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Ico; + break; + } } } - finally + else { - chunk.Data?.Dispose(); + frame = BmpDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Bmp; } - } - } - catch - { - image?.Dispose(); - throw; - } + if (frame is not null) + { + frames.Add((type, frame)); + this.Dimensions = new(Math.Max(this.Dimensions.Width, frame.Size.Width), Math.Max(this.Dimensions.Height, frame.Size.Height)); + } - throw new NotImplementedException(); + this.currentStream.Position = endPosition; + } + } } - private void ReadSeq() + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + this.currentStream = stream; - } + Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); + long dataSize = riffHeader.Size; + long dataStartPosition = this.currentStream.Position; - private bool TryReadChunk(Span buffer, out AniChunk chunk) - { - if (!this.TryReadChunkLength(buffer, out int length)) - { - // IEND - chunk = default; - return false; - } + ImageMetadata metadata = new(); + AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + + List<(ListIconChunkType Type, ImageInfo Info)> infoList = []; + this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk); + + ImageInfo imageInfo = new(this.Dimensions, metadata, (IReadOnlyList)aniMetadata.IconFrames); - while (length < 0) + foreach (int i in sequence) { - // Not a valid chunk so try again until we reach a known chunk. - if (!this.TryReadChunkLength(buffer, out length)) + (ListIconChunkType type, ImageInfo info) = infoList[i]; + + ImageFrameMetadata rootFrameMetadata = imageInfo.FrameMetadataCollection is [var first, ..] ? first : new(); + AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); + aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameCount = info.FrameMetadataCollection.Count; + aniFrameMetadata.EncodingWidth = type switch { - // IEND - chunk = default; - return false; - } + ListIconChunkType.Bmp => Narrow(info.Width), + ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingWidth, + ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingWidth, + _ => null + }; + aniFrameMetadata.EncodingHeight = type switch + { + ListIconChunkType.Bmp => Narrow(info.Height), + ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingHeight, + ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingHeight, + _ => null + }; + aniFrameMetadata.SubImageMetadata = info.Metadata; + aniMetadata.IconFrames.Add(rootFrameMetadata); } - AniChunkType type = this.ReadChunkType(buffer); + return imageInfo; - // A chunk might report a length that exceeds the length of the stream. - // Take the minimum of the two values to ensure we don't read past the end of the stream. - long position = this.currentStream.Position; - chunk = new AniChunk( - length: (int)Math.Min(length, this.currentStream.Length - position), - type: type, - data: this.ReadChunkData(length)); + void IdentifyFrameChunk() + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) + { + continue; + } - return true; - } + long endPosition = this.currentStream.Position + chunk.Size; + ImageInfo? info = null; + ListIconChunkType type = default; + if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) + { + if (this.currentStream.TryReadUnmanaged(out IconDir dir)) + { + this.currentStream.Position -= Unsafe.SizeOf(); + + switch (dir.Type) + { + case IconFileType.CUR: + info = CurDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Cur; + break; + case IconFileType.ICO: + info = IcoDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Ico; + break; + } + } + } + else + { + info = BmpDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Bmp; + } + if (info is not null) + { + infoList.Add((type, info)); + this.Dimensions = new(Math.Max(this.Dimensions.Width, info.Size.Width), Math.Max(this.Dimensions.Height, info.Size.Height)); + } - protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) - { - throw new NotImplementedException(); + this.currentStream.Position = endPosition; + } + } } - private void ReadHeader() + private AniMetadata ReadHeader(long dataStartPosition, long dataSize, ImageMetadata metadata) { - // Skip the identifier - this.currentStream.Skip(12); - Span buffer = stackalloc byte[36]; - _ = this.currentStream.Read(buffer); - this.header = AniHeader.Parse(buffer); - } + if (!this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader riffChunkHeader) || + (AniChunkType)riffChunkHeader.FourCc is not AniChunkType.AniH) + { + Guard.IsTrue(false, nameof(riffChunkHeader), "Missing ANIH chunk."); + } - private void ReadSeq(Stream stream) - { - Span buffer = stackalloc byte[4]; - int length = BinaryPrimitives.ReadInt32BigEndian(buffer); + AniMetadata aniMetadata = metadata.GetAniMetadata(); + + if (this.currentStream.TryReadUnmanaged(out AniHeader result)) + { + this.header = result; + aniMetadata.Width = result.Width; + aniMetadata.Height = result.Height; + aniMetadata.BitCount = result.BitCount; + aniMetadata.Planes = result.Planes; + aniMetadata.DisplayRate = result.DisplayRate; + aniMetadata.Flags = result.Flags; + } + return aniMetadata; } /// - /// Attempts to read the length of the next chunk. + /// Call
+ /// -> Call
+ /// -> Call ///
- /// Temporary buffer. - /// The result length. If the return type is this parameter is passed uninitialized. - /// - /// Whether the length was read. - /// - [MethodImpl(InliningOptions.ShortMethod)] - private bool TryReadChunkLength(Span buffer, out int result) + private void HandleRiffChunk(out Span sequence, out Span rate, long dataStartPosition, long dataSize, AniMetadata aniMetadata, ICollection totalFrameCount, Action handleFrameChunk) { - if (this.currentStream.Read(buffer, 0, 4) == 4) + sequence = default; + rate = default; + + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) { - result = BinaryPrimitives.ReadInt32BigEndian(buffer); + switch ((AniChunkType)chunk.FourCc) + { + case AniChunkType.Seq: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + sequence = MemoryMarshal.Cast(data.Memory.Span); + break; + } - return true; + case AniChunkType.Rate: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + rate = MemoryMarshal.Cast(data.Memory.Span); + break; + } + + case AniChunkType.List: + this.HandleListChunk(dataStartPosition, dataSize, aniMetadata, handleFrameChunk); + break; + default: + break; + } } - result = 0; - return false; + if (sequence == default) + { + sequence = Enumerable.Range(0, totalFrameCount.Count).ToArray(); + } } - /// - /// Identifies the chunk type from the chunk. - /// - /// Temporary buffer. - /// - /// Thrown if the input stream is not valid. - /// - [MethodImpl(InliningOptions.ShortMethod)] - private AniChunkType ReadChunkType(Span buffer) + private void HandleListChunk(long dataStartPosition, long dataSize, AniMetadata aniMetadata, Action handleFrameChunk) { - if (this.currentStream.Read(buffer, 0, 4) == 4) + if (!this.currentStream.TryReadUnmanaged(out uint listType)) + { + return; + } + + switch ((AniListType)listType) { - return (AniChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + case AniListType.Fram: + { + handleFrameChunk(); + break; + } + + case AniListType.Info: + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + switch ((AniListInfoType)chunk.FourCc) + { + case AniListInfoType.INam: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + aniMetadata.Name = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); + break; + } + + case AniListInfoType.IArt: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + aniMetadata.Artist = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); + break; + } + + default: + break; + } + } + + break; + } } + } - PngThrowHelper.ThrowInvalidChunkType(); + private bool TryReadChunk(long startPosition, long size, out RiffChunkHeader chunk) + { + if (this.currentStream.Position - startPosition >= size) + { + chunk = default; + return false; + } - // The IDE cannot detect the throw here. - return default; + return this.currentStream.TryReadUnmanaged(out chunk); } /// @@ -185,9 +395,9 @@ private AniChunkType ReadChunkType(Span buffer) /// /// The length of the chunk data to read. [MethodImpl(InliningOptions.ShortMethod)] - private IMemoryOwner ReadChunkData(int length) + private IMemoryOwner ReadChunkData(uint length) { - if (length == 0) + if (length is 0) { return new BasicArrayBuffer([]); } @@ -195,12 +405,13 @@ private IMemoryOwner ReadChunkData(int length) // We rent the buffer here to return it afterwards in Decode() // We don't want to throw a degenerated memory exception here as we want to allow partial decoding // so limit the length. - length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); - IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); + int len = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); + IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(len, AllocationOptions.Clean); - this.currentStream.Read(buffer.GetSpan(), 0, length); + this.currentStream.Read(buffer.GetSpan(), 0, len); return buffer; } + private static byte Narrow(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; } diff --git a/src/ImageSharp/Formats/Ani/AniFormat.cs b/src/ImageSharp/Formats/Ani/AniFormat.cs index 16f707390a..cc13e1aae8 100644 --- a/src/ImageSharp/Formats/Ani/AniFormat.cs +++ b/src/ImageSharp/Formats/Ani/AniFormat.cs @@ -1,16 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Ico; - namespace SixLabors.ImageSharp.Formats.Ani; - /// /// Registers the image encoders, decoders and mime type detectors for the bmp format. /// -public sealed class AniFormat : IImageFormat +public sealed class AniFormat : IImageFormat { /// /// Gets the shared instance. @@ -18,17 +14,20 @@ public sealed class AniFormat : IImageFormat public static AniFormat Instance { get; } = new(); /// - public AniMetadata CreateDefaultFormatMetadata() => throw new NotImplementedException(); + public string Name => "ANI"; /// - public string Name => "ANI"; + public string DefaultMimeType => "application/x-navi-animation"; + + /// + public IEnumerable MimeTypes => AniConstants.MimeTypes; /// - public string DefaultMimeType { get; } + public IEnumerable FileExtensions => AniConstants.FileExtensions; /// - public IEnumerable MimeTypes { get; } + public AniMetadata CreateDefaultFormatMetadata() => new(); /// - public IEnumerable FileExtensions { get; } + public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs new file mode 100644 index 0000000000..8b2b0b9175 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Provides Ani specific metadata information for the image. +/// +public class AniFrameMetadata : IFormatFrameMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public AniFrameMetadata() + { + } + + /// + /// Gets or sets the display time for this frame (in 1/60 seconds) + /// + public uint Rate { get; set; } + + /// + /// Gets or sets the frames count of **one** "icon" chunk. + /// + public int FrameCount { get; set; } = 1; + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingHeight { get; set; } + + /// + /// Gets or sets the of one "icon" chunk. + /// + public ImageMetadata? SubImageMetadata { get; set; } + + /// + public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => + new() + { + Rate = (uint)metadata.Duration.TotalSeconds * 60 + }; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata { Rate = this.Rate }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.Rate / 60d) }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + throw new NotImplementedException(); + } + + /// + public AniFrameMetadata DeepClone() => new() { Rate = this.Rate }; +} diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 514ce710d3..8f504c5118 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -1,11 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Ani; -[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 36)] internal readonly struct AniHeader { public uint Size { get; } @@ -26,16 +26,24 @@ internal readonly struct AniHeader public AniHeaderFlags Flags { get; } - public static AniHeader Parse(in ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref AniHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(in Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public void WriteTo(in Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } +/// +/// Flags for the ANI header. +/// [Flags] public enum AniHeaderFlags : uint { + /// + /// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file. + /// IsIcon = 1, + + /// + /// If set, the ANI file contains a "seq " chunk. + /// ContainsSeq = 2 } diff --git a/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs new file mode 100644 index 0000000000..83826da111 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Webp; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Detects ico file headers. +/// +public class AniImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => RiffOrListChunkHeader.HeaderSize; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? AniFormat.Instance : null; + return format is not null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is + { + IsRiff: true, + FormType: AniConstants.AniFourCc + }; +} diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index d373a7d9a5..72d2b83272 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; @@ -11,6 +11,51 @@ namespace SixLabors.ImageSharp.Formats.Ani; ///
public class AniMetadata : IFormatMetadata { + /// + /// Gets or sets the width of frames in the animation. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the height of frames in the animation. + /// + public uint Height { get; set; } + + /// + /// Gets or sets the number of bits per pixel. + /// + public uint BitCount { get; set; } + + /// + /// Gets or sets the number of frames in the animation. + /// + public uint Planes { get; set; } + + /// + /// Gets or sets the default display rate of frames in the animation. + /// + public uint DisplayRate { get; set; } + + /// + /// Gets or sets the flags for the ANI header. + /// + public AniHeaderFlags Flags { get; set; } + + /// + /// Gets or sets the name of the ANI file. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the artist of the ANI file. + /// + public string? Artist { get; set; } + + /// + /// Gets or sets the each "icon" chunk in ANI file. + /// + public IList IconFrames { get; set; } = []; + /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs index 32dbb4a51e..87a1f19cc7 100644 --- a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -17,7 +17,7 @@ internal enum BmpRenderingIntent /// /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. /// - LCS_GM_BUSINESS = 1, + LCS_GM_BUSINESS = 1, /// /// Maintains colorimetric match. Used for graphic designs and named colors. diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index 3e02538c84..28681e6ea3 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/src/ImageSharp/Formats/Icon/IconDir.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; @@ -25,19 +26,14 @@ internal struct IconDir(ushort reserved, IconFileType type, ushort count) /// public ushort Count = count; - public IconDir(IconFileType type) - : this(type, 0) - { - } - - public IconDir(IconFileType type, ushort count) + public IconDir(IconFileType type, ushort count = 0) : this(0, type, count) { } - public static IconDir Parse(ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref IconDir Parse(ReadOnlySpan data) + => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public readonly void WriteTo(Stream stream) + => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs index eab15dd872..598ec47b6e 100644 --- a/src/ImageSharp/Formats/Icon/IconDirEntry.cs +++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; @@ -52,9 +53,9 @@ internal struct IconDirEntry ///
public uint ImageOffset; - public static IconDirEntry Parse(in ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref IconDirEntry Parse(in ReadOnlySpan data) + => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(in Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public readonly void WriteTo(in Stream stream) + => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 03e01f912f..3965a05328 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -120,17 +120,17 @@ private void InitHeader(Image image) 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(i.Size)); - }).ToArray(), + image.Frames.Select(i => + { + IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata(); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); + }).ToArray(), IconFileType.CUR => - image.Frames.Select(i => - { - CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); - }).ToArray(), + image.Frames.Select(i => + { + CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); + }).ToArray(), _ => throw new NotSupportedException(), }; } diff --git a/src/ImageSharp/Formats/Icon/IconFileType.cs b/src/ImageSharp/Formats/Icon/IconFileType.cs index 3450698f11..3c13227d7d 100644 --- a/src/ImageSharp/Formats/Icon/IconFileType.cs +++ b/src/ImageSharp/Formats/Icon/IconFileType.cs @@ -16,5 +16,5 @@ internal enum IconFileType : ushort /// /// CUR file /// - CUR = 2, + CUR = 2 } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 39c4beb618..421b7cee33 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -100,7 +100,7 @@ public static WebpVp8X WriteTrunksBeforeData( bool hasAnimation) { // Write file size later - RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); + RiffHelper.BeginWriteRiff(stream, WebpConstants.WebpFormTypeFourCc); // Write VP8X, header if necessary. WebpVp8X vp8x = default; @@ -151,7 +151,7 @@ public static void WriteTrunksAfterData( RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition); + RiffHelper.EndWriteVp8X(stream, in vp8x, updateVp8x, initialPosition); } /// @@ -189,7 +189,7 @@ public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alp stream.WriteByte(flags); stream.Write(dataBytes); - RiffHelper.EndWriteChunk(stream, pos); + RiffHelper.EndWriteChunk(stream, pos, 2); } /// diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs index 491f716500..8133cf71d6 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -127,6 +127,6 @@ public void WriteTo(Stream stream) WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); - RiffHelper.EndWriteChunk(stream, pos); + RiffHelper.EndWriteChunk(stream, pos, 2); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index f088448391..c98efbbd46 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -325,7 +325,7 @@ public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrame if (hasAnimation) { - RiffHelper.EndWriteChunk(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition, 2); } return hasAlpha; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index e4ebe14731..f0b5af8e72 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -517,7 +517,7 @@ private bool Encode( if (hasAnimation) { - RiffHelper.EndWriteChunk(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition, 2); } } finally diff --git a/src/ImageSharp/Formats/Webp/RiffHelper.cs b/src/ImageSharp/Formats/Webp/RiffHelper.cs index b6318c7486..e40c398f94 100644 --- a/src/ImageSharp/Formats/Webp/RiffHelper.cs +++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs @@ -2,138 +2,122 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Text; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp; internal static class RiffHelper { - /// - /// The header bytes identifying RIFF file. - /// - private const uint RiffFourCc = 0x52_49_46_46; + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + long pos = BeginWriteChunk(stream, fourCc); + stream.Write(data); + EndWriteChunk(stream, pos); + } - public static void WriteRiffFile(Stream stream, string formType, Action func) => - WriteChunk(stream, RiffFourCc, s => - { - s.Write(Encoding.ASCII.GetBytes(formType)); - func(s); - }); + public static void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged => + WriteChunk(stream, fourCc, MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in chunk, 1))); - public static void WriteChunk(Stream stream, uint fourCc, Action func) + public static long BeginWriteChunk(Stream stream, ReadOnlySpan fourCc) { - Span buffer = stackalloc byte[4]; - // write the fourCC - BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); + stream.Write(fourCc); long sizePosition = stream.Position; - stream.Position += 4; - - func(stream); - - long position = stream.Position; - - uint dataSize = (uint)(position - sizePosition - 4); - // padding - if (dataSize % 2 == 1) - { - stream.WriteByte(0); - position++; - } + // Leaving the place for the size + stream.Position += 4; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); - stream.Position = sizePosition; - stream.Write(buffer); - stream.Position = position; + return sizePosition; } - public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + public static long BeginWriteChunk(Stream stream, uint fourCc) { Span buffer = stackalloc byte[4]; - - // write the fourCC BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); - uint size = (uint)data.Length; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); - stream.Write(buffer); - stream.Write(data); - - // padding - if (size % 2 is 1) - { - stream.WriteByte(0); - } + return BeginWriteChunk(stream, buffer); } - public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) - where TStruct : unmanaged + public static long BeginWriteRiff(Stream stream, ReadOnlySpan formType) { - fixed (TStruct* ptr = &chunk) - { - WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); - } + long sizePosition = BeginWriteChunk(stream, "RIFF"u8); + stream.Write(formType); + return sizePosition; } - public static long BeginWriteChunk(Stream stream, uint fourCc) + public static long BeginWriteList(Stream stream, ReadOnlySpan listType) { - Span buffer = stackalloc byte[4]; - - // write the fourCC - BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); - - long sizePosition = stream.Position; - stream.Position += 4; - + long sizePosition = BeginWriteChunk(stream, "LIST"u8); + stream.Write(listType); return sizePosition; } - public static void EndWriteChunk(Stream stream, long sizePosition) + public static void EndWriteChunk(Stream stream, long sizePosition, int alignment = 1) { - Span buffer = stackalloc byte[4]; + Guard.MustBeGreaterThan(alignment, 0, nameof(alignment)); - long position = stream.Position; + long currentPosition = stream.Position; - uint dataSize = (uint)(position - sizePosition - 4); + uint dataSize = (uint)(currentPosition - sizePosition - 4); - // padding - if (dataSize % 2 is 1) + // Add padding + while (dataSize % alignment is not 0) { stream.WriteByte(0); - position++; + dataSize++; + currentPosition++; } // Add the size of the encoded file to the Riff header. - BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); stream.Position = sizePosition; - stream.Write(buffer); - stream.Position = position; + stream.Write(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref dataSize), sizeof(uint))); + stream.Position = currentPosition; } - public static long BeginWriteRiffFile(Stream stream, string formType) + public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X, long initPosition) { - long sizePosition = BeginWriteChunk(stream, RiffFourCc); - stream.Write(Encoding.ASCII.GetBytes(formType)); - return sizePosition; - } - - public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition) - { - EndWriteChunk(stream, sizePosition + 4); + // Jump through "RIFF" fourCC + EndWriteChunk(stream, initPosition + 4, 2); // Write the VP8X chunk if necessary. - if (updateVp8x) + if (updateVp8X) { long position = stream.Position; - stream.Position = sizePosition + 12; - vp8x.WriteTo(stream); + stream.Position = initPosition + 12; + vp8X.WriteTo(stream); stream.Position = position; } } } + +internal readonly struct RiffChunkHeader +{ + public readonly uint FourCc; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); + + public readonly uint Size; +} + +internal readonly struct RiffOrListChunkHeader +{ + public const int HeaderSize = 12; + + public readonly uint FourCc; + + public readonly uint Size; + + public readonly uint FormType; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); + + public bool IsRiff => this.FourCc is 0x52_49_46_46; // "RIFF" + + public bool IsList => this.FourCc is 0x4C_49_53_54; // "LIST" + + public static ref RiffOrListChunkHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); +} diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 818c843ea9..56b989794b 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -29,36 +29,19 @@ internal static class WebpConstants }; /// - /// Signature byte which identifies a VP8L header. + /// Gets the header bytes identifying a Webp. /// - public const byte Vp8LHeaderMagicByte = 0x2F; + public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; /// - /// The header bytes identifying RIFF file. + /// Gets the header bytes identifying a Webp. /// - public static readonly byte[] RiffFourCc = - { - 0x52, // R - 0x49, // I - 0x46, // F - 0x46 // F - }; + public const uint WebpFourCc = 0x57_45_42_50; /// - /// The header bytes identifying a Webp. - /// - public static readonly byte[] WebpHeader = - { - 0x57, // W - 0x45, // E - 0x42, // B - 0x50 // P - }; - - /// - /// The header bytes identifying a Webp. + /// Signature byte which identifies a VP8L header. /// - public const string WebpFourCc = "WEBP"; + public const byte Vp8LHeaderMagicByte = 0x2F; /// /// 3 bits reserved for version. diff --git a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs index 2b91aa95fe..a7f8d43672 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 12; + public int HeaderSize => RiffOrListChunkHeader.HeaderSize; /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) @@ -21,21 +21,9 @@ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out I } private bool IsSupportedFileFormat(ReadOnlySpan header) - => header.Length >= this.HeaderSize && IsRiffContainer(header) && IsWebpFile(header); - - /// - /// Checks, if the header starts with a valid RIFF FourCC. - /// - /// The header bytes. - /// True, if its a valid RIFF FourCC. - private static bool IsRiffContainer(ReadOnlySpan header) - => header[..4].SequenceEqual(WebpConstants.RiffFourCc); - - /// - /// Checks if 'WEBP' is present in the header. - /// - /// The header bytes. - /// True, if its a webp file. - private static bool IsWebpFile(ReadOnlySpan header) - => header.Slice(8, 4).SequenceEqual(WebpConstants.WebpHeader); + => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is + { + IsRiff: true, + FormType: WebpConstants.WebpFourCc + }; } diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index d3542de7af..2dc9e81589 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -304,6 +304,26 @@ public static class ImageMetadataExtensions /// The new public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 1aa53d65e1..e1e5597dec 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -3,6 +3,12 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Ani; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Runtime.InteropServices; +using System; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.IO; @@ -22,10 +28,14 @@ internal sealed class BufferedReadStream : Stream private readonly unsafe byte* pinnedReadBuffer; - // Index within our buffer, not reader position. + /// + /// Index within our buffer, not reader position. + /// private int readBufferIndex; - // Matches what the stream position would be without buffering + /// + /// Matches what the stream position would be without buffering + /// private long readerPosition; private bool isDisposed; @@ -194,6 +204,49 @@ public override int Read(Span buffer) return this.ReadToBufferViaCopyFast(buffer); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadUnmanaged(out T result) + where T : unmanaged + { + this.cancellationToken.ThrowIfCancellationRequested(); + + int size = Unsafe.SizeOf(); + + if (size > this.BufferSize) + { + Span span = stackalloc byte[size]; + if (this.ReadToBufferDirectSlow(span) != size) + { + result = default; + return false; + } + + result = MemoryMarshal.Read(span); + } + else + { + if ((uint)this.readBufferIndex > (uint)(this.BufferSize - size)) + { + this.FillReadBuffer(); + } + + if (this.GetCopyCount(size) != size) + { + this.EofHitCount++; + result = default; + return false; + } + + Span span = this.readBuffer.AsSpan(this.readBufferIndex, size); + + this.readerPosition += size; + this.readBufferIndex += size; + result = MemoryMarshal.Read(span); + } + + return true; + } + /// public override void Flush() { diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs index f79570f22c..17439e3092 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -2,9 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Ani; -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.Ani; @@ -16,7 +13,9 @@ public class AniDecoderTests { [Theory] [WithFile(Work, PixelTypes.Rgba32)] - public void CurDecoder_Decode(TestImageProvider provider) + [WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)] + [WithFile(Help, PixelTypes.Rgba32)] + public void AniDecoder_Decode(TestImageProvider provider) { using Image image = provider.GetImage(AniDecoder.Instance); } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2284167c80..ed43cffcd2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1260,6 +1260,7 @@ public static class Cur public static class Ani { public const string Work = "Ani/Work.ani"; + public const string MultiFramesInEveryIconChunk = "Ani/aero_busy.ani"; + public const string Help = "Ani/Help.ani"; } - } diff --git a/tests/Images/Input/Ani/Help.ani b/tests/Images/Input/Ani/Help.ani new file mode 100644 index 0000000000..d623503cac --- /dev/null +++ b/tests/Images/Input/Ani/Help.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c49cbb1ca0a3f268695a80df93b1ce2b2cba335a80e8244dd3a702863159bd99 +size 12998 diff --git a/tests/Images/Input/Ani/aero_busy.ani b/tests/Images/Input/Ani/aero_busy.ani new file mode 100644 index 0000000000..e2fa9d31a5 --- /dev/null +++ b/tests/Images/Input/Ani/aero_busy.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff38afb523490e1a9f157c0447bc616b19c22df88bdb45c163243d834e9745f8 +size 556304 From 33be868b184acc8ed240c8b79a6724fb414a3ca1 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 12 Mar 2025 20:25:32 +0800 Subject: [PATCH 05/20] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 2 +- src/ImageSharp/Formats/Ani/AniHeader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index ab79a14e0a..d5507208b2 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Ani; internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options) { - private enum ListIconChunkType + private enum ListIconChunkType : byte { Ico = 1, Cur = 2, diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 8f504c5118..474aeb7a6d 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -28,7 +28,7 @@ internal readonly struct AniHeader public static ref AniHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public void WriteTo(in Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); + public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } /// From a86f5332ba6cacdba753c3f8e4cfcd157b7e7130 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 12 Mar 2025 20:43:28 +0800 Subject: [PATCH 06/20] format --- src/ImageSharp/Formats/Ani/AniConstants.cs | 21 ++++---------- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 15 +++++----- src/ImageSharp/Formats/Ani/AniHeader.cs | 17 ----------- src/ImageSharp/Formats/Ani/AniHeaderFlags.cs | 21 ++++++++++++++ src/ImageSharp/Formats/Ani/AniMetadata.cs | 14 +++++----- .../Formats/Webp/RiffChunkHeader.cs | 16 +++++++++++ src/ImageSharp/Formats/Webp/RiffHelper.cs | 28 ------------------- .../Formats/Webp/RiffOrListChunkHeader.cs | 26 +++++++++++++++++ src/ImageSharp/Formats/Webp/WebpConstants.cs | 10 +++---- src/ImageSharp/IO/BufferedReadStream.cs | 5 ---- 10 files changed, 87 insertions(+), 86 deletions(-) create mode 100644 src/ImageSharp/Formats/Ani/AniHeaderFlags.cs create mode 100644 src/ImageSharp/Formats/Webp/RiffChunkHeader.cs create mode 100644 src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs index 0b40efa31f..b5b282de55 100644 --- a/src/ImageSharp/Formats/Ani/AniConstants.cs +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -5,6 +5,11 @@ namespace SixLabors.ImageSharp.Formats.Ani; internal static class AniConstants { + /// + /// Gets the header bytes identifying an ani. + /// + public const uint AniFourCc = 0x41_43_4F_4E; + /// /// The list of mime types that equate to an ani. /// @@ -19,20 +24,4 @@ internal static class AniConstants /// Gets the header bytes identifying an ani. /// public static ReadOnlySpan AniFormTypeFourCc => "ACON"u8; - - /// - /// Gets the header bytes identifying an ani. - /// - public const uint AniFourCc = 0x41_43_4F_4E; - - public static class ChunkFourCcs - { - public static ReadOnlySpan AniHeader => "anih"u8; - - public static ReadOnlySpan Seq => "seq "u8; - - public static ReadOnlySpan Rate => "rate"u8; - - public static ReadOnlySpan Icon => "icon"u8; - } } diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index d5507208b2..71594b6f5b 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -15,19 +15,11 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Metadata; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options) { - private enum ListIconChunkType : byte - { - Ico = 1, - Cur = 2, - Bmp = 3 - } - /// /// The general decoder options. /// @@ -40,6 +32,13 @@ private enum ListIconChunkType : byte private AniHeader header; + private enum ListIconChunkType : byte + { + Ico = 1, + Cur, + Bmp + } + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.currentStream = stream; diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 474aeb7a6d..1656911cc4 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -30,20 +30,3 @@ internal readonly struct AniHeader public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } - -/// -/// Flags for the ANI header. -/// -[Flags] -public enum AniHeaderFlags : uint -{ - /// - /// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file. - /// - IsIcon = 1, - - /// - /// If set, the ANI file contains a "seq " chunk. - /// - ContainsSeq = 2 -} diff --git a/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs b/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs new file mode 100644 index 0000000000..37a27c28b3 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Flags for the ANI header. +/// +[Flags] +public enum AniHeaderFlags : uint +{ + /// + /// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file. + /// + IsIcon = 1, + + /// + /// If set, the ANI file contains a "seq " chunk. + /// + ContainsSeq = 2 +} diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index 72d2b83272..cdebcbb24f 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -11,6 +11,13 @@ namespace SixLabors.ImageSharp.Formats.Ani; ///
public class AniMetadata : IFormatMetadata { + /// + /// Initializes a new instance of the class. + /// + public AniMetadata() + { + } + /// /// Gets or sets the width of frames in the animation. /// @@ -56,13 +63,6 @@ public class AniMetadata : IFormatMetadata ///
public IList IconFrames { get; set; } = []; - /// - /// Initializes a new instance of the class. - /// - public AniMetadata() - { - } - /// public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); diff --git a/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs b/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs new file mode 100644 index 0000000000..3fd67e5359 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Webp; + +internal readonly struct RiffChunkHeader +{ + public readonly uint FourCc; + + public readonly uint Size; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); +} diff --git a/src/ImageSharp/Formats/Webp/RiffHelper.cs b/src/ImageSharp/Formats/Webp/RiffHelper.cs index e40c398f94..1a409eb8e0 100644 --- a/src/ImageSharp/Formats/Webp/RiffHelper.cs +++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs @@ -93,31 +93,3 @@ public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X } } } - -internal readonly struct RiffChunkHeader -{ - public readonly uint FourCc; - - public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); - - public readonly uint Size; -} - -internal readonly struct RiffOrListChunkHeader -{ - public const int HeaderSize = 12; - - public readonly uint FourCc; - - public readonly uint Size; - - public readonly uint FormType; - - public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); - - public bool IsRiff => this.FourCc is 0x52_49_46_46; // "RIFF" - - public bool IsList => this.FourCc is 0x4C_49_53_54; // "LIST" - - public static ref RiffOrListChunkHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); -} diff --git a/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs b/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs new file mode 100644 index 0000000000..4f12d8342e --- /dev/null +++ b/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Webp; + +internal readonly struct RiffOrListChunkHeader +{ + public const int HeaderSize = 12; + + public readonly uint FourCc; + + public readonly uint Size; + + public readonly uint FormType; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); + + public bool IsRiff => this.FourCc is 0x52_49_46_46; // "RIFF" + + public bool IsList => this.FourCc is 0x4C_49_53_54; // "LIST" + + public static ref RiffOrListChunkHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); +} diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 56b989794b..89092d3c78 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -28,11 +28,6 @@ internal static class WebpConstants 0x2A }; - /// - /// Gets the header bytes identifying a Webp. - /// - public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; - /// /// Gets the header bytes identifying a Webp. /// @@ -302,4 +297,9 @@ internal static class WebpConstants -7, 8, -8, -9 }; + + /// + /// Gets the header bytes identifying a Webp. + /// + public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index e1e5597dec..36e0bbb557 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -3,12 +3,7 @@ using System.Buffers; using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.Formats.Ani; -using static System.Runtime.InteropServices.JavaScript.JSType; using System.Runtime.InteropServices; -using System; -using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.IO; From fc9fdee0ac02bdb7acf88c02d4e666b64de27766 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 21:57:35 +0800 Subject: [PATCH 07/20] fix primary ctor --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 8 ++++++-- src/ImageSharp/Formats/Icon/IconDir.cs | 15 +++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 71594b6f5b..a4dfb5ca98 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -18,12 +18,12 @@ namespace SixLabors.ImageSharp.Formats.Ani; -internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options) +internal class AniDecoderCore : ImageDecoderCore { /// /// The general decoder options. /// - private readonly Configuration configuration = options.Configuration; + private readonly Configuration configuration; /// /// The stream to decode from. @@ -32,6 +32,10 @@ internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options private AniHeader header; + public AniDecoderCore(DecoderOptions options) + : base(options) => + this.configuration = options.Configuration; + private enum ListIconChunkType : byte { Ico = 1, diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index 28681e6ea3..45781295a8 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/src/ImageSharp/Formats/Icon/IconDir.cs @@ -7,30 +7,37 @@ namespace SixLabors.ImageSharp.Formats.Icon; [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] -internal struct IconDir(ushort reserved, IconFileType type, ushort count) +internal struct IconDir { public const int Size = 3 * sizeof(ushort); /// /// Reserved. Must always be 0. /// - public ushort Reserved = reserved; + public ushort Reserved; /// /// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. /// - public IconFileType Type = type; + public IconFileType Type; /// /// Specifies number of images in the file. /// - public ushort Count = count; + public ushort Count; public IconDir(IconFileType type, ushort count = 0) : this(0, type, count) { } + public IconDir(ushort reserved, IconFileType type, ushort count) + { + this.Reserved = reserved; + this.Type = type; + this.Count = count; + } + public static ref IconDir Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); From 7377d3c6df2ebf81e7cd5830ae485d02774c10e9 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 21:59:32 +0800 Subject: [PATCH 08/20] fix decoder modifier --- src/ImageSharp/Formats/Ani/AniDecoder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoder.cs b/src/ImageSharp/Formats/Ani/AniDecoder.cs index 9794867feb..5cc06ae157 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoder.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoder.cs @@ -4,7 +4,10 @@ namespace SixLabors.ImageSharp.Formats.Ani; -internal sealed class AniDecoder : ImageDecoder +/// +/// Decoder for generating an image out of an ani encoded stream. +/// +public sealed class AniDecoder : ImageDecoder { private AniDecoder() { @@ -15,6 +18,7 @@ private AniDecoder() /// public static AniDecoder Instance { get; } = new(); + /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); @@ -24,8 +28,10 @@ protected override Image Decode(DecoderOptions options, Stream s 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)); From c0ad4994cba3026a51f7c6dbf76fb0f7a5a33d4c Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:01:37 +0800 Subject: [PATCH 09/20] use overload ctor --- src/ImageSharp/Formats/Icon/IconDir.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index 45781295a8..36d5d8820e 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/src/ImageSharp/Formats/Icon/IconDir.cs @@ -26,7 +26,12 @@ internal struct IconDir ///
public ushort Count; - public IconDir(IconFileType type, ushort count = 0) + public IconDir(IconFileType type) + : this(type, 0) + { + } + + public IconDir(IconFileType type, ushort count) : this(0, type, count) { } From a979b96abc86d5185c17c2bfaf654c27ba88769c Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:03:40 +0800 Subject: [PATCH 10/20] rename FrameDelay --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 4 ++-- src/ImageSharp/Formats/Ani/AniFrameMetadata.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index a4dfb5ca98..561eec5977 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -114,7 +114,7 @@ protected override Image Decode(BufferedReadStream stream, Cance ImageFrameMetadata rootFrameMetadata = img.Frames.RootFrame.Metadata; AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameDelay = rate == default ? aniMetadata.DisplayRate : rate[i]; aniFrameMetadata.FrameCount = img.Frames.Count; aniFrameMetadata.EncodingWidth = encodingWidth; aniFrameMetadata.EncodingHeight = encodingHeight; @@ -201,7 +201,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok ImageFrameMetadata rootFrameMetadata = imageInfo.FrameMetadataCollection is [var first, ..] ? first : new(); AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameDelay = rate == default ? aniMetadata.DisplayRate : rate[i]; aniFrameMetadata.FrameCount = info.FrameMetadataCollection.Count; aniFrameMetadata.EncodingWidth = type switch { diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 8b2b0b9175..62f12a3fdf 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -21,7 +21,7 @@ public AniFrameMetadata() /// /// Gets or sets the display time for this frame (in 1/60 seconds) /// - public uint Rate { get; set; } + public uint FrameDelay { get; set; } /// /// Gets or sets the frames count of **one** "icon" chunk. @@ -49,14 +49,14 @@ public AniFrameMetadata() public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => new() { - Rate = (uint)metadata.Duration.TotalSeconds * 60 + FrameDelay = (uint)metadata.Duration.TotalSeconds * 60 }; /// - IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata { Rate = this.Rate }; + IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata { FrameDelay = this.FrameDelay }; /// - public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.Rate / 60d) }; + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; /// public void AfterFrameApply(ImageFrame source, ImageFrame destination) @@ -66,5 +66,5 @@ public void AfterFrameApply(ImageFrame source, ImageFrame - public AniFrameMetadata DeepClone() => new() { Rate = this.Rate }; + public AniFrameMetadata DeepClone() => new() { FrameDelay = this.FrameDelay }; } From 163bdd408a26de1d3078ae0a55810e387447a82b Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:07:03 +0800 Subject: [PATCH 11/20] fix DeepClone --- .../Formats/Ani/AniFrameMetadata.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 62f12a3fdf..723ca00e5f 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -53,7 +53,15 @@ public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; /// - IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata { FrameDelay = this.FrameDelay }; + IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata + { + FrameDelay = this.FrameDelay, + EncodingHeight = this.EncodingHeight, + EncodingWidth = this.EncodingWidth, + FrameCount = this.FrameCount + + // TODO SubImageMetadata + }; /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; @@ -66,5 +74,13 @@ public void AfterFrameApply(ImageFrame source, ImageFrame - public AniFrameMetadata DeepClone() => new() { FrameDelay = this.FrameDelay }; + public AniFrameMetadata DeepClone() => new() + { + FrameDelay = this.FrameDelay, + EncodingHeight = this.EncodingHeight, + EncodingWidth = this.EncodingWidth, + FrameCount = this.FrameCount + + // TODO SubImageMetadata + }; } From 95f13cc541b45a2400691c40eb45bf0abb25dce7 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:07:54 +0800 Subject: [PATCH 12/20] AfterFrameApply --- src/ImageSharp/Formats/Ani/AniFrameMetadata.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 723ca00e5f..8a567c40ad 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -70,7 +70,6 @@ public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin public void AfterFrameApply(ImageFrame source, ImageFrame destination) where TPixel : unmanaged, IPixel { - throw new NotImplementedException(); } /// From fe95636f350ba2f643f008a80c0946d7088b2390 Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:09:42 +0800 Subject: [PATCH 13/20] Span.IsEmpty --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 561eec5977..7ce8292a79 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -114,7 +114,7 @@ protected override Image Decode(BufferedReadStream stream, Cance ImageFrameMetadata rootFrameMetadata = img.Frames.RootFrame.Metadata; AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.FrameDelay = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[i]; aniFrameMetadata.FrameCount = img.Frames.Count; aniFrameMetadata.EncodingWidth = encodingWidth; aniFrameMetadata.EncodingHeight = encodingHeight; @@ -201,7 +201,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok ImageFrameMetadata rootFrameMetadata = imageInfo.FrameMetadataCollection is [var first, ..] ? first : new(); AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.FrameDelay = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[i]; aniFrameMetadata.FrameCount = info.FrameMetadataCollection.Count; aniFrameMetadata.EncodingWidth = type switch { @@ -331,7 +331,7 @@ private void HandleRiffChunk(out Span sequence, out Span rate, long d } } - if (sequence == default) + if (sequence.IsEmpty) { sequence = Enumerable.Range(0, totalFrameCount.Count).ToArray(); } From ed0edfe20a62f3f3d15be52ad508d9915cf15e4b Mon Sep 17 00:00:00 2001 From: Poker Date: Mon, 7 Apr 2025 22:45:28 +0800 Subject: [PATCH 14/20] fix AniMetadata --- .../Formats/Ani/AniFrameMetadata.cs | 10 +---- src/ImageSharp/Formats/Ani/AniMetadata.cs | 41 ++++++++++++++++--- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 8a567c40ad..9288994762 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -53,15 +53,7 @@ public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin }; /// - IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata - { - FrameDelay = this.FrameDelay, - EncodingHeight = this.EncodingHeight, - EncodingWidth = this.EncodingWidth, - FrameCount = this.FrameCount - - // TODO SubImageMetadata - }; + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index cdebcbb24f..9b30bedb6b 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -21,21 +21,33 @@ public AniMetadata() /// /// Gets or sets the width of frames in the animation. /// + /// + /// Remains zero when has flag + /// public uint Width { get; set; } /// /// Gets or sets the height of frames in the animation. /// + /// + /// Remains zero when has flag + /// public uint Height { get; set; } /// /// Gets or sets the number of bits per pixel. /// + /// + /// Remains zero when has flag + /// public uint BitCount { get; set; } /// /// Gets or sets the number of frames in the animation. /// + /// + /// Remains zero when has flag + /// public uint Planes { get; set; } /// @@ -64,21 +76,38 @@ public AniMetadata() public IList IconFrames { get; set; } = []; /// - public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + => throw new NotImplementedException(); /// public void AfterImageApply(Image destination) - where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + where TPixel : unmanaged, IPixel + { + } /// - public IDeepCloneable DeepClone() => throw new NotImplementedException(); + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + public PixelTypeInfo GetPixelTypeInfo() + => throw new NotImplementedException(); /// - public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => throw new NotImplementedException(); /// - AniMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); + public AniMetadata DeepClone() => new() + { + Width = this.Width, + Height = this.Height, + BitCount = this.BitCount, + Planes = this.Planes, + DisplayRate = this.DisplayRate, + Flags = this.Flags, + Name = this.Name, + Artist = this.Artist + + // TODO IconFrames + }; } From ceb0d51fa90d0503b4a74515eb081bb4057f4932 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 8 Apr 2025 17:47:06 +0800 Subject: [PATCH 15/20] apply new metadata layout --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 126 ++++++++++-------- .../Formats/Ani/AniFrameMetadata.cs | 29 +++- src/ImageSharp/Formats/Ani/AniMetadata.cs | 5 - 3 files changed, 93 insertions(+), 67 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 7ce8292a79..539c3db2d5 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -59,12 +59,17 @@ protected override Image Decode(BufferedReadStream stream, Cance List> list = []; - foreach (int i in sequence) + for (int i = 0; i < sequence.Length; i++) { - (ListIconChunkType type, Image? img) = frames[i]; - byte? encodingWidth = null; - byte? encodingHeight = null; - bool isRootFrame = true; + int sequenceIndex = sequence[i]; + (ListIconChunkType type, Image? img) = frames[sequenceIndex]; + + AniFrameMetadata aniFrameMetadata = new() + { + FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex], + SequenceNumber = i + }; + list.AddRange(img.Frames.Select(source => { ImageFrame target = new(this.Options.Configuration, this.Dimensions); @@ -73,53 +78,34 @@ protected override Image Decode(BufferedReadStream stream, Cance source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); } + AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); + source.Metadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); switch (type) { case ListIconChunkType.Ico: IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); - target.Metadata.SetFormatMetadata(IcoFormat.Instance, icoFrameMetadata); - if (isRootFrame) - { - encodingWidth ??= icoFrameMetadata.EncodingWidth; - encodingHeight ??= icoFrameMetadata.EncodingHeight; - } - + // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); + clonedMetadata.IcoFrameMetadata = icoFrameMetadata; + clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; + clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; break; case ListIconChunkType.Cur: CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); - target.Metadata.SetFormatMetadata(CurFormat.Instance, curFrameMetadata); - if (isRootFrame) - { - encodingWidth ??= curFrameMetadata.EncodingWidth; - encodingHeight ??= curFrameMetadata.EncodingHeight; - } - + // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); + clonedMetadata.CurFrameMetadata = curFrameMetadata; + clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; + clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; break; case ListIconChunkType.Bmp: - if (isRootFrame) - { - encodingWidth = Narrow(source.Width); - encodingHeight = Narrow(source.Height); - } - + clonedMetadata.EncodingWidth = Narrow(source.Width); + clonedMetadata.EncodingHeight = Narrow(source.Height); break; default: break; } - isRootFrame = false; - return target; })); - - ImageFrameMetadata rootFrameMetadata = img.Frames.RootFrame.Metadata; - AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[i]; - aniFrameMetadata.FrameCount = img.Frames.Count; - aniFrameMetadata.EncodingWidth = encodingWidth; - aniFrameMetadata.EncodingHeight = encodingHeight; - aniFrameMetadata.SubImageMetadata = img.Metadata; - aniMetadata.IconFrames.Add(rootFrameMetadata); } foreach ((ListIconChunkType _, Image img) in frames) @@ -193,34 +179,62 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok List<(ListIconChunkType Type, ImageInfo Info)> infoList = []; this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk); - ImageInfo imageInfo = new(this.Dimensions, metadata, (IReadOnlyList)aniMetadata.IconFrames); + List frameMetadataCollection = new(sequence.Length); - foreach (int i in sequence) + for (int i = 0; i < sequence.Length; i++) { - (ListIconChunkType type, ImageInfo info) = infoList[i]; + int sequenceIndex = sequence[i]; + (ListIconChunkType type, ImageInfo info) = infoList[sequenceIndex]; - ImageFrameMetadata rootFrameMetadata = imageInfo.FrameMetadataCollection is [var first, ..] ? first : new(); - AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); - aniFrameMetadata.FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[i]; - aniFrameMetadata.FrameCount = info.FrameMetadataCollection.Count; - aniFrameMetadata.EncodingWidth = type switch + AniFrameMetadata aniFrameMetadata = new() { - ListIconChunkType.Bmp => Narrow(info.Width), - ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingWidth, - ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingWidth, - _ => null + FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex], + SequenceNumber = i }; - aniFrameMetadata.EncodingHeight = type switch + + if (info.FrameMetadataCollection.Count is not 0) { - ListIconChunkType.Bmp => Narrow(info.Height), - ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingHeight, - ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingHeight, - _ => null - }; - aniFrameMetadata.SubImageMetadata = info.Metadata; - aniMetadata.IconFrames.Add(rootFrameMetadata); + frameMetadataCollection.AddRange( + info.FrameMetadataCollection.Select(frameMetadata => + { + AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); + frameMetadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); + switch (type) + { + case ListIconChunkType.Ico: + IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); + // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); + clonedMetadata.IcoFrameMetadata = icoFrameMetadata; + clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; + clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; + break; + case ListIconChunkType.Cur: + CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); + // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); + clonedMetadata.CurFrameMetadata = curFrameMetadata; + clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; + clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; + break; + default: + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(type), "FrameMetadata must be ICO or CUR"); + break; + } + + return frameMetadata; + })); + } + else // BMP + { + aniFrameMetadata.EncodingWidth = Narrow(info.Width); + aniFrameMetadata.EncodingHeight = Narrow(info.Height); + ImageFrameMetadata frameMetadata = new(); + frameMetadata.SetFormatMetadata(AniFormat.Instance, aniFrameMetadata); + frameMetadataCollection.Add(frameMetadata); + } } + ImageInfo imageInfo = new(this.Dimensions, metadata, frameMetadataCollection); + return imageInfo; void IdentifyFrameChunk() diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 9288994762..6e7f220bd6 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Metadata; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; @@ -24,9 +26,9 @@ public AniFrameMetadata() public uint FrameDelay { get; set; } /// - /// Gets or sets the frames count of **one** "icon" chunk. + /// Gets or sets the sequence number of current frame. /// - public int FrameCount { get; set; } = 1; + public int SequenceNumber { get; set; } = 1; /// /// Gets or sets the encoding width.
@@ -41,9 +43,21 @@ public AniFrameMetadata() public byte? EncodingHeight { get; set; } /// - /// Gets or sets the of one "icon" chunk. + /// Gets or sets a value indicating whether the frame will be encoded as an ICO or CUR file. /// - public ImageMetadata? SubImageMetadata { get; set; } + [MemberNotNullWhen(true, nameof(IcoFrameMetadata))] + [MemberNotNullWhen(false, nameof(CurFrameMetadata))] + public bool IsIco { get; set; } + + /// + /// Gets or sets the of one "icon" chunk. + /// + public IcoFrameMetadata? IcoFrameMetadata { get; set; } + + /// + /// Gets or sets the of one "icon" chunk. + /// + public CurFrameMetadata? CurFrameMetadata { get; set; } /// public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => @@ -70,7 +84,10 @@ public void AfterFrameApply(ImageFrame source, ImageFrame public string? Artist { get; set; } - /// - /// Gets or sets the each "icon" chunk in ANI file. - /// - public IList IconFrames { get; set; } = []; - /// public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); From 256934344c9100e660ee02603cd6268a4a328451 Mon Sep 17 00:00:00 2001 From: Poker Date: Tue, 8 Apr 2025 18:00:20 +0800 Subject: [PATCH 16/20] update AniFrameFormat --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 46 +++++++++---------- src/ImageSharp/Formats/Ani/AniFrameFormat.cs | 25 ++++++++++ .../Formats/Ani/AniFrameMetadata.cs | 6 +-- 3 files changed, 48 insertions(+), 29 deletions(-) create mode 100644 src/ImageSharp/Formats/Ani/AniFrameFormat.cs diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 539c3db2d5..57f9d4031f 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -36,13 +36,6 @@ public AniDecoderCore(DecoderOptions options) : base(options) => this.configuration = options.Configuration; - private enum ListIconChunkType : byte - { - Ico = 1, - Cur, - Bmp - } - protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.currentStream = stream; @@ -54,7 +47,7 @@ protected override Image Decode(BufferedReadStream stream, Cance ImageMetadata metadata = new(); AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); - List<(ListIconChunkType Type, Image Image)> frames = []; + List<(AniFrameFormat Type, Image Image)> frames = []; this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk); List> list = []; @@ -62,7 +55,7 @@ protected override Image Decode(BufferedReadStream stream, Cance for (int i = 0; i < sequence.Length; i++) { int sequenceIndex = sequence[i]; - (ListIconChunkType type, Image? img) = frames[sequenceIndex]; + (AniFrameFormat type, Image? img) = frames[sequenceIndex]; AniFrameMetadata aniFrameMetadata = new() { @@ -80,23 +73,24 @@ protected override Image Decode(BufferedReadStream stream, Cance AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); source.Metadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); + clonedMetadata.FrameFormat = type; switch (type) { - case ListIconChunkType.Ico: + case AniFrameFormat.Ico: IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); clonedMetadata.IcoFrameMetadata = icoFrameMetadata; clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; break; - case ListIconChunkType.Cur: + case AniFrameFormat.Cur: CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); clonedMetadata.CurFrameMetadata = curFrameMetadata; clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; break; - case ListIconChunkType.Bmp: + case AniFrameFormat.Bmp: clonedMetadata.EncodingWidth = Narrow(source.Width); clonedMetadata.EncodingHeight = Narrow(source.Height); break; @@ -108,7 +102,7 @@ protected override Image Decode(BufferedReadStream stream, Cance })); } - foreach ((ListIconChunkType _, Image img) in frames) + foreach ((AniFrameFormat _, Image img) in frames) { img.Dispose(); } @@ -128,7 +122,7 @@ void DecodeFrameChunk() long endPosition = this.currentStream.Position + chunk.Size; Image? frame = null; - ListIconChunkType type = default; + AniFrameFormat type = default; if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) { if (this.currentStream.TryReadUnmanaged(out IconDir dir)) @@ -139,11 +133,11 @@ void DecodeFrameChunk() { case IconFileType.CUR: frame = CurDecoder.Instance.Decode(this.Options, this.currentStream); - type = ListIconChunkType.Cur; + type = AniFrameFormat.Cur; break; case IconFileType.ICO: frame = IcoDecoder.Instance.Decode(this.Options, this.currentStream); - type = ListIconChunkType.Ico; + type = AniFrameFormat.Ico; break; } } @@ -151,7 +145,7 @@ void DecodeFrameChunk() else { frame = BmpDecoder.Instance.Decode(this.Options, this.currentStream); - type = ListIconChunkType.Bmp; + type = AniFrameFormat.Bmp; } if (frame is not null) @@ -176,7 +170,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok ImageMetadata metadata = new(); AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); - List<(ListIconChunkType Type, ImageInfo Info)> infoList = []; + List<(AniFrameFormat Type, ImageInfo Info)> infoList = []; this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk); List frameMetadataCollection = new(sequence.Length); @@ -184,7 +178,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok for (int i = 0; i < sequence.Length; i++) { int sequenceIndex = sequence[i]; - (ListIconChunkType type, ImageInfo info) = infoList[sequenceIndex]; + (AniFrameFormat type, ImageInfo info) = infoList[sequenceIndex]; AniFrameMetadata aniFrameMetadata = new() { @@ -199,16 +193,17 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok { AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); frameMetadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); + clonedMetadata.FrameFormat = type; switch (type) { - case ListIconChunkType.Ico: + case AniFrameFormat.Ico: IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); clonedMetadata.IcoFrameMetadata = icoFrameMetadata; clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; break; - case ListIconChunkType.Cur: + case AniFrameFormat.Cur: CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); clonedMetadata.CurFrameMetadata = curFrameMetadata; @@ -227,6 +222,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok { aniFrameMetadata.EncodingWidth = Narrow(info.Width); aniFrameMetadata.EncodingHeight = Narrow(info.Height); + aniFrameMetadata.FrameFormat = type; ImageFrameMetadata frameMetadata = new(); frameMetadata.SetFormatMetadata(AniFormat.Instance, aniFrameMetadata); frameMetadataCollection.Add(frameMetadata); @@ -248,7 +244,7 @@ void IdentifyFrameChunk() long endPosition = this.currentStream.Position + chunk.Size; ImageInfo? info = null; - ListIconChunkType type = default; + AniFrameFormat type = default; if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) { if (this.currentStream.TryReadUnmanaged(out IconDir dir)) @@ -259,11 +255,11 @@ void IdentifyFrameChunk() { case IconFileType.CUR: info = CurDecoder.Instance.Identify(this.Options, this.currentStream); - type = ListIconChunkType.Cur; + type = AniFrameFormat.Cur; break; case IconFileType.ICO: info = IcoDecoder.Instance.Identify(this.Options, this.currentStream); - type = ListIconChunkType.Ico; + type = AniFrameFormat.Ico; break; } } @@ -271,7 +267,7 @@ void IdentifyFrameChunk() else { info = BmpDecoder.Instance.Identify(this.Options, this.currentStream); - type = ListIconChunkType.Bmp; + type = AniFrameFormat.Bmp; } if (info is not null) diff --git a/src/ImageSharp/Formats/Ani/AniFrameFormat.cs b/src/ImageSharp/Formats/Ani/AniFrameFormat.cs new file mode 100644 index 0000000000..661aae7fc3 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniFrameFormat.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Specifies the format of the frame data. +/// +public enum AniFrameFormat +{ + /// + /// The frame data is in ICO format. + /// + Ico = 1, + + /// + /// The frame data is in CUR format. + /// + Cur, + + /// + /// The frame data is in BMP format. + /// + Bmp +} diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 6e7f220bd6..f024c1c8ad 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -43,11 +43,9 @@ public AniFrameMetadata() public byte? EncodingHeight { get; set; } /// - /// Gets or sets a value indicating whether the frame will be encoded as an ICO or CUR file. + /// Gets or sets a value indicating whether the frame will be encoded as an ICO or CUR or BMP file. /// - [MemberNotNullWhen(true, nameof(IcoFrameMetadata))] - [MemberNotNullWhen(false, nameof(CurFrameMetadata))] - public bool IsIco { get; set; } + public AniFrameFormat FrameFormat { get; set; } /// /// Gets or sets the of one "icon" chunk. From 6fcf75c5d92068f4ea026b0125d0c386c0db2747 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 4 Feb 2026 12:43:35 +1000 Subject: [PATCH 17/20] Fix merge issues --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 9 ++++++++- src/ImageSharp/Formats/Ani/AniFrameMetadata.cs | 13 +++++++++---- src/ImageSharp/Formats/Ani/AniMetadata.cs | 6 +++--- src/ImageSharp/Formats/Webp/WebpConstants.cs | 10 +++++----- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 57f9d4031f..e1cdba2ec1 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -78,6 +78,7 @@ protected override Image Decode(BufferedReadStream stream, Cance { case AniFrameFormat.Ico: IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); + // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); clonedMetadata.IcoFrameMetadata = icoFrameMetadata; clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; @@ -85,6 +86,7 @@ protected override Image Decode(BufferedReadStream stream, Cance break; case AniFrameFormat.Cur: CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); + // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); clonedMetadata.CurFrameMetadata = curFrameMetadata; clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; @@ -198,6 +200,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok { case AniFrameFormat.Ico: IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); + // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); clonedMetadata.IcoFrameMetadata = icoFrameMetadata; clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; @@ -205,6 +208,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok break; case AniFrameFormat.Cur: CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); + // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); clonedMetadata.CurFrameMetadata = curFrameMetadata; clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; @@ -218,8 +222,9 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok return frameMetadata; })); } - else // BMP + else { + // BMP aniFrameMetadata.EncodingWidth = Narrow(info.Width); aniFrameMetadata.EncodingHeight = Narrow(info.Height); aniFrameMetadata.FrameFormat = type; @@ -253,6 +258,7 @@ void IdentifyFrameChunk() switch (dir.Type) { + // TODO: Use Core decoders. case IconFileType.CUR: info = CurDecoder.Instance.Identify(this.Options, this.currentStream); type = AniFrameFormat.Cur; @@ -266,6 +272,7 @@ void IdentifyFrameChunk() } else { + // TODO: Use Core decoders. info = BmpDecoder.Instance.Identify(this.Options, this.currentStream); type = AniFrameFormat.Bmp; } diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index f024c1c8ad..5af0b6fc7c 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; +using System.Numerics; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.PixelFormats; @@ -68,12 +68,17 @@ public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + // TODO: Implement. You need to consider encoding width/height. + return new() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; + } /// - public void AfterFrameApply(ImageFrame source, ImageFrame destination) + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { + // TODO: Implement. You need to consider encoding width/height. } /// @@ -83,7 +88,7 @@ public void AfterFrameApply(ImageFrame source, ImageFrame throw new NotImplementedException(); /// - public void AfterImageApply(Image destination) - where TPixel : unmanaged, IPixel + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel { } diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index b42e5313e2..7bee3c8c50 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -60,11 +60,6 @@ internal static class WebpConstants 0x50 // P ]; - /// - /// The header bytes identifying a Webp. - /// - public const string WebpFourCc = "WEBP"; - /// /// 3 bits reserved for version. /// @@ -324,4 +319,9 @@ internal static class WebpConstants -7, 8, -8, -9 ]; + + /// + /// Gets the header bytes identifying a Webp. + /// + public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; } From db5b03d894ad6798fa6005ecc4fd215545100e41 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 27 Jul 2026 12:20:23 +1000 Subject: [PATCH 18/20] Complete ANI, ICO, and CUR codec support --- src/ImageSharp/Common/InlineArray.cs | 52 + src/ImageSharp/Common/InlineArray.tt | 2 +- .../Compression/Zlib/ChunkedReadStream.cs | 15 +- .../Compression/Zlib/ZlibInflateReader.cs | 11 +- src/ImageSharp/Configuration.cs | 5 +- src/ImageSharp/Formats/Ani/AniChunkType.cs | 41 +- .../Formats/Ani/AniConfigurationModule.cs | 11 +- src/ImageSharp/Formats/Ani/AniConstants.cs | 28 +- src/ImageSharp/Formats/Ani/AniDecoder.cs | 19 +- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 947 ++++++++++++------ src/ImageSharp/Formats/Ani/AniEncoder.cs | 24 + src/ImageSharp/Formats/Ani/AniEncoderCore.cs | 468 +++++++++ src/ImageSharp/Formats/Ani/AniFormat.cs | 9 +- src/ImageSharp/Formats/Ani/AniFrameFormat.cs | 12 +- .../Formats/Ani/AniFrameMetadata.cs | 223 ++++- src/ImageSharp/Formats/Ani/AniFrameStream.cs | 114 +++ src/ImageSharp/Formats/Ani/AniHeader.cs | 94 +- src/ImageSharp/Formats/Ani/AniHeaderFlags.cs | 6 +- .../Formats/Ani/AniImageFormatDetector.cs | 27 +- src/ImageSharp/Formats/Ani/AniMetadata.cs | 96 +- .../Formats/Ani/AniRiffChunkHeader.cs | 34 + src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 70 +- .../Formats/Cur/CurConfigurationModule.cs | 6 +- src/ImageSharp/Formats/Cur/CurConstants.cs | 10 +- src/ImageSharp/Formats/Cur/CurDecoder.cs | 7 +- src/ImageSharp/Formats/Cur/CurDecoderCore.cs | 10 +- src/ImageSharp/Formats/Cur/CurEncoder.cs | 5 +- src/ImageSharp/Formats/Cur/CurEncoderCore.cs | 33 + src/ImageSharp/Formats/Cur/CurFormat.cs | 9 +- .../Formats/Cur/CurFrameMetadata.cs | 52 +- .../Formats/Cur/CurImageFormatDetector.cs | 49 + src/ImageSharp/Formats/Cur/CurMetadata.cs | 6 +- .../Decompressors/B44ExrCompression.cs | 32 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 32 +- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 30 +- .../Formats/Ico/IcoConfigurationModule.cs | 6 +- src/ImageSharp/Formats/Ico/IcoConstants.cs | 10 +- src/ImageSharp/Formats/Ico/IcoDecoder.cs | 7 +- src/ImageSharp/Formats/Ico/IcoDecoderCore.cs | 18 +- src/ImageSharp/Formats/Ico/IcoEncoder.cs | 5 +- src/ImageSharp/Formats/Ico/IcoEncoderCore.cs | 33 + src/ImageSharp/Formats/Ico/IcoFormat.cs | 5 +- .../Formats/Ico/IcoFrameMetadata.cs | 49 +- .../Formats/Ico/IcoImageFormatDetector.cs | 49 + src/ImageSharp/Formats/Ico/IcoMetadata.cs | 6 +- .../Formats/Icon/IconDecoderCore.cs | 415 ++++---- src/ImageSharp/Formats/Icon/IconDir.cs | 38 +- src/ImageSharp/Formats/Icon/IconDirEntry.cs | 25 +- .../Formats/Icon/IconEncoderCore.cs | 248 +++-- src/ImageSharp/Formats/Icon/IconFileType.cs | 6 +- .../Formats/Icon/IconFrameCompression.cs | 6 +- .../Formats/Icon/IconFrameStream.cs | 114 +++ .../Formats/Icon/IconImageFormatDetector.cs | 66 -- .../Decoder/ArithmeticScanDecoder.cs | 7 +- .../Jpeg/Components/Decoder/HuffmanTable.cs | 12 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 42 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 6 +- .../Formats/Webp/Chunks/WebpVp8X.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 19 +- src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs | 10 +- .../Formats/Webp/Lossy/Vp8Decoder.cs | 12 + .../Formats/Webp/Lossy/Vp8Encoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Matrix.cs | 12 +- .../Formats/Webp/Lossy/WebpLossyDecoder.cs | 2 +- .../Formats/Webp/Lossy/YuvConversion.cs | 8 +- .../Formats/Webp/RiffChunkHeader.cs | 16 - src/ImageSharp/Formats/Webp/RiffHelper.cs | 126 ++- .../Formats/Webp/RiffOrListChunkHeader.cs | 26 - src/ImageSharp/Formats/Webp/WebpConstants.cs | 15 +- .../Formats/Webp/WebpImageFormatDetector.cs | 24 +- .../_Generated/ImageExtensions.Save.cs | 103 ++ .../_Generated/ImageMetadataExtensions.cs | 78 +- .../Formats/_Generated/_Formats.ttinclude | 2 + src/ImageSharp/IO/BufferedReadStream.cs | 52 +- tests/Directory.Build.targets | 6 +- tests/ImageSharp.Tests/ConfigurationTests.cs | 2 +- .../Formats/Ani/AniDecoderTests.cs | 134 ++- .../Formats/Ani/AniEncoderTests.cs | 156 +++ .../Formats/Ani/AniMetadataTests.cs | 31 + .../Formats/Icon/Cur/CurDecoderTests.cs | 40 + .../Formats/Icon/Cur/CurEncoderTests.cs | 36 + .../Formats/Icon/Ico/IcoDecoderTests.cs | 48 + .../Formats/ImageFormatManagerTests.cs | 3 + 84 files changed, 3542 insertions(+), 1187 deletions(-) create mode 100644 src/ImageSharp/Formats/Ani/AniEncoder.cs create mode 100644 src/ImageSharp/Formats/Ani/AniEncoderCore.cs create mode 100644 src/ImageSharp/Formats/Ani/AniFrameStream.cs create mode 100644 src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs create mode 100644 src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/Icon/IconFrameStream.cs delete mode 100644 src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs delete mode 100644 src/ImageSharp/Formats/Webp/RiffChunkHeader.cs delete mode 100644 src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs create mode 100644 tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs create mode 100644 tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs diff --git a/src/ImageSharp/Common/InlineArray.cs b/src/ImageSharp/Common/InlineArray.cs index 778981fd96..700551a8f3 100644 --- a/src/ImageSharp/Common/InlineArray.cs +++ b/src/ImageSharp/Common/InlineArray.cs @@ -26,6 +26,15 @@ internal struct InlineArray8 private T t; } +/// +/// Represents a safe, fixed sized buffer of 14 elements. +/// +[InlineArray(14)] +internal struct InlineArray14 +{ + private T t; +} + /// /// Represents a safe, fixed sized buffer of 16 elements. /// @@ -35,4 +44,47 @@ internal struct InlineArray16 private T t; } +/// +/// Represents a safe, fixed sized buffer of 18 elements. +/// +[InlineArray(18)] +internal struct InlineArray18 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 19 elements. +/// +[InlineArray(19)] +internal struct InlineArray19 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 26 elements. +/// +[InlineArray(26)] +internal struct InlineArray26 +{ + private T t; +} +/// +/// Represents a safe, fixed sized buffer of 36 elements. +/// +[InlineArray(36)] +internal struct InlineArray36 +{ + private T t; +} + +/// +/// Represents a safe, fixed sized buffer of 256 elements. +/// +[InlineArray(256)] +internal struct InlineArray256 +{ + private T t; +} diff --git a/src/ImageSharp/Common/InlineArray.tt b/src/ImageSharp/Common/InlineArray.tt index 6dc17b9a9c..d689b0469a 100644 --- a/src/ImageSharp/Common/InlineArray.tt +++ b/src/ImageSharp/Common/InlineArray.tt @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp; <#GenerateInlineArrays();#> <#+ -private static int[] Lengths = [4, 8, 16 ]; +private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256]; void GenerateInlineArrays() { diff --git a/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs b/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs index b697327fff..9d1bd9a734 100644 --- a/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs +++ b/src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs @@ -77,13 +77,14 @@ public override int ReadByte() } /// - public override int Read(byte[] buffer, int offset, int count) + public override int Read(byte[] buffer, int offset, int count) => this.Read(buffer.AsSpan(offset, count)); + + /// + public override int Read(Span buffer) { - // Decrement currentDataRemaining only by bytes actually returned by - // innerStream.Read; a short read otherwise underflows the segment - // counter and triggers getData() before the segment is truly drained. + // Decrement currentDataRemaining only by bytes actually returned by innerStream.Read; a short read otherwise advances segments too early. int totalBytesRead = 0; - while (totalBytesRead < count) + while (totalBytesRead < buffer.Length) { if (this.currentDataRemaining is 0) { @@ -94,8 +95,8 @@ public override int Read(byte[] buffer, int offset, int count) } } - int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); - int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead); + int bytesToRead = Math.Min(buffer.Length - totalBytesRead, this.currentDataRemaining); + int bytesRead = this.innerStream.Read(buffer.Slice(totalBytesRead, bytesToRead)); if (bytesRead is 0) { break; diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs index de69717da4..493ec27380 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs @@ -14,13 +14,6 @@ namespace SixLabors.ImageSharp.Compression.Zlib; /// internal sealed class ZlibInflateReader : IDisposable { - /// - /// Used to read the Adler-32 and Crc-32 checksums. - /// We don't actually use this for anything so it doesn't - /// have to be threadsafe. - /// - private static readonly byte[] ChecksumBuffer = new byte[4]; - private readonly ChunkedReadStream segmentStream; public ZlibInflateReader(BufferedReadStream innerStream) @@ -112,7 +105,9 @@ private bool InitializeInflateStream(bool isCriticalChunk) { // We don't need this for inflate so simply skip by the next four bytes. // https://tools.ietf.org/html/rfc1950#page-6 - if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4) + InlineArray4 checksumBuffer = default; + + if (this.segmentStream.Read(checksumBuffer) != 4) { return false; } diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 1e5433b9e2..94073dc099 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Exr; @@ -224,6 +225,7 @@ public void Configure(IImageFormatConfigurationModule configuration) /// . /// . /// . + /// . ///
/// The default configuration of . internal static Configuration CreateDefaultInstance() => new( @@ -238,5 +240,6 @@ public void Configure(IImageFormatConfigurationModule configuration) new ExrConfigurationModule(), new QoiConfigurationModule(), new IcoConfigurationModule(), - new CurConfigurationModule()); + new CurConfigurationModule(), + new AniConfigurationModule()); } diff --git a/src/ImageSharp/Formats/Ani/AniChunkType.cs b/src/ImageSharp/Formats/Ani/AniChunkType.cs index 35eba01140..b50b1c3684 100644 --- a/src/ImageSharp/Formats/Ani/AniChunkType.cs +++ b/src/ImageSharp/Formats/Ani/AniChunkType.cs @@ -3,68 +3,71 @@ namespace SixLabors.ImageSharp.Formats.Ani; +/// +/// Identifies top-level ANI RIFF chunks. +/// internal enum AniChunkType : uint { /// - /// "anih" + /// The animation header chunk, "anih". /// - AniH = 0x68_69_6E_61, + Header = 0x68_69_6E_61, /// - /// "seq " + /// The frame sequence chunk, "seq ". /// - Seq = 0x20_71_65_73, + Sequence = 0x20_71_65_73, /// - /// "rate" + /// The per-step display-rate chunk, "rate". /// Rate = 0x65_74_61_72, /// - /// "LIST" + /// A RIFF list chunk, "LIST". /// List = 0x54_53_49_4C } /// -/// ListType +/// Identifies ANI RIFF list types. /// internal enum AniListType : uint { /// - /// "INFO" (ListType) + /// The information list, "INFO". /// Info = 0x4F_46_4E_49, /// - /// "fram" + /// The embedded frame-resource list, "fram". /// - Fram = 0x6D_61_72_66 + Frames = 0x6D_61_72_66 } /// -/// in "INFO" +/// Identifies chunks stored in an ANI information list. /// -internal enum AniListInfoType : uint +internal enum AniInfoChunkType : uint { /// - /// "INAM" + /// The animation name, "INAM". /// - INam = 0x4D_41_4E_49, + Name = 0x4D_41_4E_49, /// - /// "IART" + /// The animation artist, "IART". /// - IArt = 0x54_52_41_49 + Artist = 0x54_52_41_49 } /// -/// in "Fram" +/// Identifies chunks stored in an ANI frame list. /// -internal enum AniListFrameType : uint +internal enum AniFrameChunkType : uint { /// - /// "icon" + /// An embedded frame resource, "icon". /// Icon = 0x6E_6F_63_69 } diff --git a/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs index 6cf3a55f10..27068c7192 100644 --- a/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs +++ b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs @@ -4,14 +4,21 @@ namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Registers the image encoders, decoders and mime type detectors for the Ico format. +/// Registers the image encoder, decoder, and format detector for the ANI format. /// public sealed class AniConfigurationModule : IImageFormatConfigurationModule { + /// + /// Initializes a new instance of the class. + /// + public AniConfigurationModule() + { + } + /// public void Configure(Configuration configuration) { - // configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder()); + configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder()); configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance); configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector()); } diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs index b5b282de55..16925cc657 100644 --- a/src/ImageSharp/Formats/Ani/AniConstants.cs +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -3,25 +3,43 @@ namespace SixLabors.ImageSharp.Formats.Ani; +/// +/// Defines constants used by the ANI format. +/// internal static class AniConstants { /// - /// Gets the header bytes identifying an ani. + /// The number of bytes in the RIFF identifier, size, and form type. /// - public const uint AniFourCc = 0x41_43_4F_4E; + public const int RiffHeaderSize = 12; /// - /// The list of mime types that equate to an ani. + /// The number of bytes in a RIFF chunk identifier and size. + /// + public const int ChunkHeaderSize = 8; + + /// + /// The number of bytes required to identify an embedded ICO or CUR resource. + /// + public const int IconDirHeaderSize = 6; + + /// + /// The list of MIME types that identify ANI data. /// public static readonly IEnumerable MimeTypes = ["application/x-navi-animation"]; /// - /// The list of file extensions that equate to an ani. + /// The list of file extensions that identify ANI data. /// public static readonly IEnumerable FileExtensions = ["ani"]; /// - /// Gets the header bytes identifying an ani. + /// Gets the RIFF container identifier. + /// + public static ReadOnlySpan RiffFourCc => "RIFF"u8; + + /// + /// Gets the ANI RIFF form type. /// public static ReadOnlySpan AniFormTypeFourCc => "ACON"u8; } diff --git a/src/ImageSharp/Formats/Ani/AniDecoder.cs b/src/ImageSharp/Formats/Ani/AniDecoder.cs index 5cc06ae157..756b281c9a 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoder.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoder.cs @@ -1,14 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. + using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Decoder for generating an image out of an ani encoded stream. +/// Decodes Windows animated cursor images. /// public sealed class AniDecoder : ImageDecoder { + /// + /// Prevents a default instance of the class from being created. + /// private AniDecoder() { } @@ -23,19 +27,26 @@ protected override Image Decode(DecoderOptions options, Stream s { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - Image image = new AniDecoderCore(options).Decode(options.Configuration, stream, cancellationToken); + + using AniDecoderCore decoder = new(options); + Image image = decoder.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 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 AniDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + + using AniDecoderCore decoder = new(options); + return decoder.Identify(options.Configuration, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index e1cdba2ec1..5015efc255 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -2,436 +2,815 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Collections; -using System.Runtime.CompilerServices; +using System.Buffers.Binary; using System.Runtime.InteropServices; using System.Text; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Icon; -using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; -internal class AniDecoderCore : ImageDecoderCore +/// +/// Performs ANI decoding and identification. +/// +internal sealed class AniDecoderCore : ImageDecoderCore, IDisposable { + private readonly List<(long Start, long End)> frameLists = new(1); + private readonly ImageMetadata imageMetadata; + private readonly AniMetadata aniMetadata; + private AniHeader header; + private IMemoryOwner? sequence; + private IMemoryOwner? rates; + /// - /// The general decoder options. + /// Reusable storage for the fixed ANI header and smaller RIFF values. /// - private readonly Configuration configuration; + private InlineArray36 buffer; /// - /// The stream to decode from. + /// Initializes a new instance of the class. /// - private BufferedReadStream currentStream = null!; - - private AniHeader header; - + /// The general decoder options. public AniDecoderCore(DecoderOptions options) - : base(options) => - this.configuration = options.Configuration; + : base(options) + { + // The decoded ANI metadata must belong to the same ImageMetadata instance transferred to Image or ImageInfo. + this.imageMetadata = new ImageMetadata(); + this.aniMetadata = this.imageMetadata.GetAniMetadata(); + } + /// protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { - this.currentStream = stream; + this.ParseContainer(stream); - Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); - long dataSize = riffHeader.Size; - long dataStartPosition = this.currentStream.Position; + DecoderOptions frameOptions = this.CreateFrameDecoderOptions(); + List<(AniFrameFormat Format, Image Image)?> resources = []; + List> outputFrames = []; - ImageMetadata metadata = new(); - AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + // Until Image accepts the frame collection, this method remains responsible for disposing every constructed output frame. + bool outputFramesOwned = false; - List<(AniFrameFormat Type, Image Image)> frames = []; - this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk); + try + { + // Container parsing runs first because seq/rate chunks can occur after the frame list and affect how resources are projected. + resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames)); + this.ProcessFrameChunks(stream, resources, (format, frameStream) => + { + cancellationToken.ThrowIfCancellationRequested(); - List> list = []; + Image resource = DecodeFrame(format, frameOptions, frameStream, cancellationToken); + this.Dimensions = new(Math.Max(this.Dimensions.Width, resource.Width), Math.Max(this.Dimensions.Height, resource.Height)); - for (int i = 0; i < sequence.Length; i++) - { - int sequenceIndex = sequence[i]; - (AniFrameFormat type, Image? img) = frames[sequenceIndex]; + return resource; + }); - AniFrameMetadata aniFrameMetadata = new() + if (resources.Count is 0) { - FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex], - SequenceNumber = i - }; + throw new InvalidImageContentException("The ANI file does not contain any frame resources."); + } + + // Keep the owners alive and resolve their spans once; sequence and rate lookup occurs for every animation step. + IMemoryOwner? sequenceOwner = this.sequence; + bool hasSequence = sequenceOwner is not null; + ReadOnlySpan sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan(); + ReadOnlySpan rates = this.rates is null ? [] : this.rates.GetSpan(); + int stepCount = hasSequence ? sequence.Length : resources.Count; + int maxFrames = (int)this.Options.MaxFrames; + outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count)); - list.AddRange(img.Frames.Select(source => + for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++) { - ImageFrame target = new(this.Options.Configuration, this.Dimensions); - for (int y = 0; y < source.Height; y++) - { - source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); - } + cancellationToken.ThrowIfCancellationRequested(); - AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); - source.Metadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); - clonedMetadata.FrameFormat = type; - switch (type) + uint resourceIndex = hasSequence ? sequence[step] : (uint)step; + if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource) { - case AniFrameFormat.Ico: - IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); - - // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); - clonedMetadata.IcoFrameMetadata = icoFrameMetadata; - clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; - clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; - break; - case AniFrameFormat.Cur: - CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); + // A bad ordering entry is recoverable ancillary data: the remaining valid steps can still be decoded. + this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource.")); - // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); - clonedMetadata.CurFrameMetadata = curFrameMetadata; - clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; - clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; - break; - case AniFrameFormat.Bmp: - clonedMetadata.EncodingWidth = Narrow(source.Width); - clonedMetadata.EncodingHeight = Narrow(source.Height); - break; - default: - break; + continue; } - return target; - })); - } + (AniFrameFormat format, Image resourceImage) = resource; + uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate; - foreach ((AniFrameFormat _, Image img) in frames) - { - img.Dispose(); - } + for (int i = 0; i < resourceImage.Frames.Count && outputFrames.Count < maxFrames; i++) + { + ImageFrame source = resourceImage.Frames[i]; + ImageFrame target = new(this.Options.Configuration, this.Dimensions); - Image image = new(this.Options.Configuration, metadata, list); + // ANI flattens differently sized ICO/CUR variants into one ImageSharp frame collection. + // The common canvas preserves that invariant, while encoding dimensions retain the source size. + for (int y = 0; y < source.Height; y++) + { + source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + } - return image; + AniFrameMetadata metadata = CreateFrameMetadata(source.Metadata, format, step + 1, frameDelay, source.Size); + target.Metadata.SetFormatMetadata(AniFormat.Instance, metadata); + outputFrames.Add(target); + } + } - void DecodeFrameChunk() - { - while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + if (outputFrames.Count is 0) { - if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) - { - continue; - } + throw new InvalidImageContentException("The ANI file does not contain any decodable animation steps."); + } - long endPosition = this.currentStream.Position + chunk.Size; - Image? frame = null; - AniFrameFormat type = default; - if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) - { - if (this.currentStream.TryReadUnmanaged(out IconDir dir)) - { - this.currentStream.Position -= Unsafe.SizeOf(); + // Image takes ownership of the supplied frames; only the temporary decoded resources remain locally owned. + Image image = new(this.Options.Configuration, this.imageMetadata, outputFrames); + outputFramesOwned = true; - switch (dir.Type) - { - case IconFileType.CUR: - frame = CurDecoder.Instance.Decode(this.Options, this.currentStream); - type = AniFrameFormat.Cur; - break; - case IconFileType.ICO: - frame = IcoDecoder.Instance.Decode(this.Options, this.currentStream); - type = AniFrameFormat.Ico; - break; - } - } - } - else + return image; + } + finally + { + // Embedded images are temporary resource containers; their pixels have already been copied to the flattened output frames. + foreach ((AniFrameFormat Format, Image Image)? resource in resources) + { + if (resource is { } value) { - frame = BmpDecoder.Instance.Decode(this.Options, this.currentStream); - type = AniFrameFormat.Bmp; + value.Image.Dispose(); } + } - if (frame is not null) + // Construction failures occur before Image can own the frames, so the partial collection must be released here. + if (!outputFramesOwned) + { + foreach (ImageFrame frame in outputFrames) { - frames.Add((type, frame)); - this.Dimensions = new(Math.Max(this.Dimensions.Width, frame.Size.Width), Math.Max(this.Dimensions.Height, frame.Size.Height)); + frame.Dispose(); } - - this.currentStream.Position = endPosition; } } } + /// protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - this.currentStream = stream; + this.ParseContainer(stream); - Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); - long dataSize = riffHeader.Size; - long dataStartPosition = this.currentStream.Position; + DecoderOptions frameOptions = this.CreateFrameDecoderOptions(); + List<(AniFrameFormat Format, ImageInfo Info)?> resources = []; + resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames)); + this.ProcessFrameChunks(stream, resources, (format, frameStream) => + { + cancellationToken.ThrowIfCancellationRequested(); - ImageMetadata metadata = new(); - AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + ImageInfo info = IdentifyFrame(format, frameOptions, frameStream, cancellationToken); + this.Dimensions = new(Math.Max(this.Dimensions.Width, info.Width), Math.Max(this.Dimensions.Height, info.Height)); - List<(AniFrameFormat Type, ImageInfo Info)> infoList = []; - this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk); + return info; + }); - List frameMetadataCollection = new(sequence.Length); + if (resources.Count is 0) + { + throw new InvalidImageContentException("The ANI file does not contain any frame resources."); + } - for (int i = 0; i < sequence.Length; i++) + // Identification mirrors decode without allocating pixels, while preserving the same step-to-resource projection. + List outputFrames = []; + IMemoryOwner? sequenceOwner = this.sequence; + bool hasSequence = sequenceOwner is not null; + ReadOnlySpan sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan(); + ReadOnlySpan rates = this.rates is null ? [] : this.rates.GetSpan(); + int stepCount = hasSequence ? sequence.Length : resources.Count; + int maxFrames = (int)this.Options.MaxFrames; + _ = outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count)); + + for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++) { - int sequenceIndex = sequence[i]; - (AniFrameFormat type, ImageInfo info) = infoList[sequenceIndex]; + cancellationToken.ThrowIfCancellationRequested(); - AniFrameMetadata aniFrameMetadata = new() + uint resourceIndex = hasSequence ? sequence[step] : (uint)step; + if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource) { - FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex], - SequenceNumber = i - }; + // Sequence errors are ancillary during identification for the same reason as decoding: other steps remain usable. + this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource.")); - if (info.FrameMetadataCollection.Count is not 0) - { - frameMetadataCollection.AddRange( - info.FrameMetadataCollection.Select(frameMetadata => - { - AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone(); - frameMetadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata); - clonedMetadata.FrameFormat = type; - switch (type) - { - case AniFrameFormat.Ico: - IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata(); - - // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null); - clonedMetadata.IcoFrameMetadata = icoFrameMetadata; - clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth; - clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight; - break; - case AniFrameFormat.Cur: - CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata(); - - // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null); - clonedMetadata.CurFrameMetadata = curFrameMetadata; - clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth; - clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight; - break; - default: - ThrowHelper.ThrowArgumentOutOfRangeException(nameof(type), "FrameMetadata must be ICO or CUR"); - break; - } + continue; + } - return frameMetadata; - })); + (AniFrameFormat format, ImageInfo info) = resource; + uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate; + + if (info.FrameMetadataCollection.Count is 0) + { + // Some embedded decoders expose only resource-level dimensions, so synthesize the one required ANI frame entry. + ImageFrameMetadata target = new(); + target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(null, format, step + 1, frameDelay, info.Size)); + outputFrames.Add(target); + continue; } - else + + for (int i = 0; i < info.FrameMetadataCollection.Count && outputFrames.Count < maxFrames; i++) { - // BMP - aniFrameMetadata.EncodingWidth = Narrow(info.Width); - aniFrameMetadata.EncodingHeight = Narrow(info.Height); - aniFrameMetadata.FrameFormat = type; - ImageFrameMetadata frameMetadata = new(); - frameMetadata.SetFormatMetadata(AniFormat.Instance, aniFrameMetadata); - frameMetadataCollection.Add(frameMetadata); + ImageFrameMetadata source = info.FrameMetadataCollection[i]; + ImageFrameMetadata target = new(); + target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(source, format, step + 1, frameDelay, info.Size)); + outputFrames.Add(target); } } - ImageInfo imageInfo = new(this.Dimensions, metadata, frameMetadataCollection); + if (outputFrames.Count is 0) + { + throw new InvalidImageContentException("The ANI file does not contain any identifiable animation steps."); + } - return imageInfo; + return new ImageInfo(this.Dimensions, this.imageMetadata, outputFrames); + } - void IdentifyFrameChunk() + /// + /// Parses the RIFF container and records frame-list boundaries for subsequent embedded decoding. + /// + /// The ANI stream. + private void ParseContainer(BufferedReadStream stream) + { + // Parser-owned chunk state is replaced by the container currently being scanned. + this.frameLists.Clear(); + this.sequence?.Dispose(); + this.rates?.Dispose(); + this.sequence = null; + this.rates = null; + + long containerStart = stream.Position; + Span riffHeader = this.buffer[..AniConstants.RiffHeaderSize]; + ReadExactly(stream, riffHeader, "RIFF header"); + + if (!riffHeader[..4].SequenceEqual(AniConstants.RiffFourCc) + || !riffHeader.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc)) { - while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) - { - if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) - { - continue; - } + throw new InvalidImageContentException("The stream does not contain an ANI RIFF container."); + } - long endPosition = this.currentStream.Position + chunk.Size; - ImageInfo? info = null; - AniFrameFormat type = default; - if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) - { - if (this.currentStream.TryReadUnmanaged(out IconDir dir)) - { - this.currentStream.Position -= Unsafe.SizeOf(); + uint declaredSize = BinaryPrimitives.ReadUInt32LittleEndian(riffHeader[4..]); + if (declaredSize < sizeof(uint)) + { + throw new InvalidImageContentException("The ANI RIFF container size is invalid."); + } - switch (dir.Type) - { - // TODO: Use Core decoders. - case IconFileType.CUR: - info = CurDecoder.Instance.Identify(this.Options, this.currentStream); - type = AniFrameFormat.Cur; - break; - case IconFileType.ICO: - info = IcoDecoder.Instance.Identify(this.Options, this.currentStream); - type = AniFrameFormat.Ico; - break; - } - } - } - else - { - // TODO: Use Core decoders. - info = BmpDecoder.Instance.Identify(this.Options, this.currentStream); - type = AniFrameFormat.Bmp; - } + // RIFF size excludes the initial identifier and size field. Some real-world ANI files incorrectly + // include those eight bytes, so the physical stream length remains the hard read boundary. + long declaredEnd = checked(containerStart + 8 + declaredSize); + long containerEnd = Math.Min(declaredEnd, stream.Length); + bool headerFound = false; - if (info is not null) - { - infoList.Add((type, info)); - this.Dimensions = new(Math.Max(this.Dimensions.Width, info.Size.Width), Math.Max(this.Dimensions.Height, info.Size.Height)); - } + while (stream.Position + AniConstants.ChunkHeaderSize <= containerEnd) + { + AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); + long dataEnd = GetChunkDataEnd(stream, chunk.Size, containerEnd); - this.currentStream.Position = endPosition; + switch ((AniChunkType)chunk.FourCc) + { + case AniChunkType.Header: + this.ReadAniHeader(stream, chunk.Size); + headerFound = true; + break; + case AniChunkType.Sequence: + // Ordering and timing affect presentation, not pixel decoding, so malformed chunks follow ancillary handling. + this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "sequence", ref this.sequence)); + break; + case AniChunkType.Rate: + this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "rate", ref this.rates)); + break; + case AniChunkType.List: + this.ReadList(stream, dataEnd); + break; } + + stream.Position = GetPaddedEnd(dataEnd, chunk.Size, containerEnd); + } + + if (!headerFound) + { + throw new InvalidImageContentException("The ANI file does not contain an animation header."); } } - private AniMetadata ReadHeader(long dataStartPosition, long dataSize, ImageMetadata metadata) + /// + /// Parses the mandatory 36-byte ANI header and copies its observable values to image metadata. + /// + /// The ANI stream. + /// The ANI header chunk size. + private void ReadAniHeader(BufferedReadStream stream, uint chunkSize) { - if (!this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader riffChunkHeader) || - (AniChunkType)riffChunkHeader.FourCc is not AniChunkType.AniH) + if (chunkSize < AniHeader.Size) { - Guard.IsTrue(false, nameof(riffChunkHeader), "Missing ANIH chunk."); + throw new InvalidImageContentException("The ANI animation header is truncated."); } - AniMetadata aniMetadata = metadata.GetAniMetadata(); + Span data = this.buffer; + ReadExactly(stream, data, "ANI header"); + this.header = AniHeader.Parse(data); - if (this.currentStream.TryReadUnmanaged(out AniHeader result)) + if (this.header.BytesInHeader < AniHeader.Size) { - this.header = result; - aniMetadata.Width = result.Width; - aniMetadata.Height = result.Height; - aniMetadata.BitCount = result.BitCount; - aniMetadata.Planes = result.Planes; - aniMetadata.DisplayRate = result.DisplayRate; - aniMetadata.Flags = result.Flags; + throw new InvalidImageContentException("The ANI animation header declares an invalid size."); } - return aniMetadata; + this.aniMetadata.Width = this.header.Width; + this.aniMetadata.Height = this.header.Height; + this.aniMetadata.BitCount = this.header.BitCount; + this.aniMetadata.Planes = this.header.Planes; + this.aniMetadata.DisplayRate = this.header.DisplayRate; + this.aniMetadata.Flags = this.header.Flags; } /// - /// Call
- /// -> Call
- /// -> Call + /// Reads a RIFF list type and records or parses its contents. ///
- private void HandleRiffChunk(out Span sequence, out Span rate, long dataStartPosition, long dataSize, AniMetadata aniMetadata, ICollection totalFrameCount, Action handleFrameChunk) + /// The ANI stream. + /// The exclusive end of the list payload. + private void ReadList(BufferedReadStream stream, long listEnd) { - sequence = default; - rate = default; + if (listEnd - stream.Position < sizeof(uint)) + { + throw new InvalidImageContentException("The ANI file contains a truncated RIFF list."); + } + + Span typeData = this.buffer[..sizeof(uint)]; + ReadExactly(stream, typeData, "RIFF list type"); + AniListType type = (AniListType)BinaryPrimitives.ReadUInt32LittleEndian(typeData); - while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + switch (type) { - switch ((AniChunkType)chunk.FourCc) + case AniListType.Frames: + // Defer nested decoding until the complete container has supplied any later seq/rate chunks. + this.frameLists.Add((stream.Position, listEnd)); + break; + case AniListType.Info when !this.Options.SkipMetadata: + this.ExecuteAncillarySegmentAction(() => this.ReadInfoList(stream, listEnd)); + break; + } + } + + /// + /// Parses the optional ANI name and artist information. + /// + /// The ANI stream. + /// The exclusive end of the information list. + private void ReadInfoList(BufferedReadStream stream, long listEnd) + { + // INAM and IART are consumed sequentially, so one grow-only buffer covers every text chunk in the list. + IMemoryOwner? textOwner = null; + + try + { + while (stream.Position + AniConstants.ChunkHeaderSize <= listEnd) { - case AniChunkType.Seq: + AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); + long dataEnd = GetChunkDataEnd(stream, chunk.Size, listEnd); + + switch ((AniInfoChunkType)chunk.FourCc) { - using IMemoryOwner data = this.ReadChunkData(chunk.Size); - sequence = MemoryMarshal.Cast(data.Memory.Span); - break; + case AniInfoChunkType.Name: + if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? name)) + { + this.aniMetadata.Name = name; + } + + break; + case AniInfoChunkType.Artist: + if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? artist)) + { + this.aniMetadata.Artist = artist; + } + + break; } - case AniChunkType.Rate: + stream.Position = GetPaddedEnd(dataEnd, chunk.Size, listEnd); + } + } + finally + { + textOwner?.Dispose(); + } + } + + /// + /// Reads a sequence or rate chunk into reusable allocator-owned memory. + /// + /// The ANI stream. + /// The chunk payload size. + /// The chunk description used in error messages. + /// The buffer to reuse or replace. + private void ReadUInt32Values(BufferedReadStream stream, uint chunkSize, string description, ref IMemoryOwner? owner) + { + // seq and rate payloads are DWORD arrays; trailing bytes cannot form a valid entry. + if (chunkSize % sizeof(uint) is not 0) + { + this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk has an invalid size."); + return; + } + + int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames); + IMemoryOwner valuesOwner; + bool replaceOwner; + + // Duplicate chunks can overwrite an equal-sized allocation. A different size uses a replacement so a failed read + // leaves the last valid chunk available to non-strict decoding. + if (owner is not null && owner.GetSpan().Length == count) + { + valuesOwner = owner; + replaceOwner = false; + } + else + { + valuesOwner = this.Options.Configuration.MemoryAllocator.Allocate(count); + replaceOwner = true; + } + + bool success = false; + + // A newly allocated replacement is not published until the entire payload has been read and normalized. + try + { + Span values = valuesOwner.GetSpan()[..count]; + Span data = MemoryMarshal.AsBytes(values); + if (stream.Read(data) != data.Length) + { + this.ThrowOrIgnoreNonStrictSegmentError($"Not enough bytes to read the ANI {description} chunk."); + return; + } + + if (!BitConverter.IsLittleEndian) + { + // RIFF integers are always little-endian; normalize once here so hot step loops use native uint indexing. + for (int i = 0; i < values.Length; i++) { - using IMemoryOwner data = this.ReadChunkData(chunk.Size); - rate = MemoryMarshal.Cast(data.Memory.Span); - break; + values[i] = BinaryPrimitives.ReverseEndianness(values[i]); } + } - case AniChunkType.List: - this.HandleListChunk(dataStartPosition, dataSize, aniMetadata, handleFrameChunk); - break; - default: - break; + success = true; + } + finally + { + if (!success && replaceOwner) + { + valuesOwner.Dispose(); } } - if (sequence.IsEmpty) + if (replaceOwner) { - sequence = Enumerable.Range(0, totalFrameCount.Count).ToArray(); + owner?.Dispose(); + owner = valuesOwner; } } - private void HandleListChunk(long dataStartPosition, long dataSize, AniMetadata aniMetadata, Action handleFrameChunk) + /// + public void Dispose() + { + this.sequence?.Dispose(); + this.rates?.Dispose(); + this.sequence = null; + this.rates = null; + } + + /// + /// Tries to read a null-terminated ANI information string. + /// + /// The ANI stream. + /// The text chunk payload size. + /// The reusable text buffer. + /// The decoded ASCII text when successful. + /// when the text was read successfully; otherwise, . + private bool TryReadText(BufferedReadStream stream, uint chunkSize, ref IMemoryOwner? owner, out string? value) { - if (!this.currentStream.TryReadUnmanaged(out uint listType)) + value = null; + + if (chunkSize > int.MaxValue) { - return; + this.ThrowOrIgnoreNonStrictSegmentError("The ANI information text chunk is too large."); + return false; + } + + int length = (int)chunkSize; + + // Retain the largest text buffer encountered because INFO values are decoded one at a time. + if (owner is null || owner.GetSpan().Length < length) + { + owner?.Dispose(); + owner = this.Options.Configuration.MemoryAllocator.Allocate(length); } - switch ((AniListType)listType) + Span data = owner.GetSpan()[..length]; + if (stream.Read(data) != data.Length) { - case AniListType.Fram: + this.ThrowOrIgnoreNonStrictSegmentError("Not enough bytes to read the ANI information text."); + return false; + } + + // RIFF text is null-terminated, but the declared chunk may include bytes after the first terminator. + int terminator = data.IndexOf((byte)0); + value = Encoding.ASCII.GetString(terminator < 0 ? data : data[..terminator]); + return true; + } + + /// + /// Processes each embedded frame-resource chunk without allowing its decoder to read adjacent RIFF data. + /// + /// The parsed resource type. + /// The ANI stream. + /// The destination resource slots. + /// The operation to perform for each resource format and bounded stream. + private void ProcessFrameChunks(BufferedReadStream stream, List<(AniFrameFormat Format, T Resource)?> resources, Func action) + where T : class + { + // Child decoding is synchronous, so one bounded stream object can be repositioned for every physical resource. + AniFrameStream frameStream = new(stream); + ReadOnlySpan sequence = this.sequence is null ? [] : this.sequence.GetSpan(); + bool hasSequence = this.sequence is not null; + int decodedResourceCount = 0; + int maxDecodedResources = (int)this.Options.MaxFrames; + uint lastRequiredResource = 0; + + if (hasSequence) + { + if (sequence.IsEmpty) { - handleFrameChunk(); - break; + return; } - case AniListType.Info: + // Only resources referenced by the retained sequence steps can contribute to the bounded output frame set. + for (int i = 0; i < sequence.Length; i++) { - while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + lastRequiredResource = Math.Max(lastRequiredResource, sequence[i]); + } + } + + foreach ((long start, long end) in this.frameLists) + { + stream.Position = start; + + while (stream.Position + AniConstants.ChunkHeaderSize <= end) + { + AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); + long dataStart = stream.Position; + long dataEnd = GetChunkDataEnd(stream, chunk.Size, end); + + if ((AniFrameChunkType)chunk.FourCc is AniFrameChunkType.Icon) { - switch ((AniListInfoType)chunk.FourCc) + int resourceIndex = resources.Count; + + // Sequence entries index the physical resource table, so ignored corrupt resources retain an empty slot. + resources.Add(null); + + // Unsequenced resources are consumed in physical order; sequenced files need only the referenced indices. + bool shouldDecode = !hasSequence || sequence.Contains((uint)resourceIndex); + if (shouldDecode) { - case AniListInfoType.INam: + this.ExecuteImageDataSegmentAction(() => { - using IMemoryOwner data = this.ReadChunkData(chunk.Size); - aniMetadata.Name = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); - break; - } + // Child decoders may seek according to embedded offsets; the bounded view prevents crossing the icon chunk. + frameStream.Reset(dataStart, chunk.Size); + AniFrameFormat format = this.GetFrameFormat(frameStream); + + // Format probing consumes the directory prefix, while the selected child decoder requires the complete resource. + frameStream.Position = 0; + resources[resourceIndex] = (format, action(format, frameStream)); + }); - case AniListInfoType.IArt: + if (resources[resourceIndex] is not null) { - using IMemoryOwner data = this.ReadChunkData(chunk.Size); - aniMetadata.Artist = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); - break; + decodedResourceCount++; } + } - default: - break; + // Every decoded resource contributes at least one output frame, while a sequence cannot reference later indices. + if ((!hasSequence && decodedResourceCount == maxDecodedResources) + || (hasSequence && (uint)resourceIndex == lastRequiredResource)) + { + return; } } - break; + stream.Position = GetPaddedEnd(dataEnd, chunk.Size, end); } } } - private bool TryReadChunk(long startPosition, long size, out RiffChunkHeader chunk) + /// + /// Determines the embedded resource format from the ANI header and ICO/CUR directory prefix. + /// + /// The bounded frame-resource stream. + /// The embedded resource format. + private AniFrameFormat GetFrameFormat(Stream stream) { - if (this.currentStream.Position - startPosition >= size) + // Without AF_ICON, the icon chunk payload is a raw DIB and has no ICO/CUR directory prefix to inspect. + if (!this.header.Flags.HasFlag(AniHeaderFlags.IsIcon)) { - chunk = default; - return false; + return AniFrameFormat.Bmp; + } + + Span iconHeader = this.buffer[..AniConstants.IconDirHeaderSize]; + if (stream.Read(iconHeader) != iconHeader.Length) + { + throw new InvalidImageContentException("The ANI file contains a truncated ICO or CUR resource."); + } + + IconFileType type = (IconFileType)BinaryPrimitives.ReadUInt16LittleEndian(iconHeader[2..]); + return type switch + { + IconFileType.ICO => AniFrameFormat.Ico, + IconFileType.CUR => AniFrameFormat.Cur, + _ => throw new InvalidImageContentException("The ANI file contains an unsupported icon resource.") + }; + } + + /// + /// Decodes one embedded ANI frame resource. + /// + /// The destination pixel type. + /// The embedded resource format. + /// The nested decoder options. + /// The bounded resource stream. + /// The token to monitor for cancellation requests. + /// The decoded resource. + private static Image DecodeFrame(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => format switch + { + AniFrameFormat.Ico => new IcoDecoderCore(options).Decode(options.Configuration, stream, cancellationToken), + AniFrameFormat.Cur => new CurDecoderCore(options).Decode(options.Configuration, stream, cancellationToken), + AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions + { + GeneralOptions = options, + SkipFileHeader = true + }).Decode(options.Configuration, stream, cancellationToken), + _ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.") + }; + + /// + /// Identifies one embedded ANI frame resource. + /// + /// The embedded resource format. + /// The nested decoder options. + /// The bounded resource stream. + /// The token to monitor for cancellation requests. + /// The identified resource. + private static ImageInfo IdentifyFrame(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => format switch + { + AniFrameFormat.Ico => new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken), + AniFrameFormat.Cur => new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken), + AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions + { + GeneralOptions = options, + SkipFileHeader = true + }).Identify(options.Configuration, stream, cancellationToken), + _ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.") + }; + + /// + /// Creates ANI metadata for one flattened output frame. + /// + /// The embedded frame metadata, when available. + /// The embedded resource format. + /// The animation sequence number. + /// The display rate in sixtieths of a second. + /// The embedded frame size. + /// The ANI frame metadata. + private static AniFrameMetadata CreateFrameMetadata(ImageFrameMetadata? source, AniFrameFormat format, int sequenceNumber, uint frameDelay, Size size) + { + AniFrameMetadata metadata = new() + { + FrameDelay = frameDelay, + SequenceNumber = sequenceNumber, + FrameFormat = format + }; + + if (source is null) + { + metadata.EncodingWidth = NarrowDimension(size.Width); + metadata.EncodingHeight = NarrowDimension(size.Height); + + return metadata; + } + + // ColorTable is managed read-only memory and remains valid after the temporary child image is disposed, + // so the flattened metadata can retain the same view without cloning its backing array. + switch (format) + { + case AniFrameFormat.Ico: + IcoFrameMetadata icoMetadata = source.GetIcoMetadata(); + metadata.EncodingWidth = icoMetadata.EncodingWidth; + metadata.EncodingHeight = icoMetadata.EncodingHeight; + metadata.Compression = icoMetadata.Compression; + metadata.BmpBitsPerPixel = icoMetadata.BmpBitsPerPixel; + metadata.ColorTable = icoMetadata.ColorTable; + + break; + case AniFrameFormat.Cur: + CurFrameMetadata curMetadata = source.GetCurMetadata(); + metadata.EncodingWidth = curMetadata.EncodingWidth; + metadata.EncodingHeight = curMetadata.EncodingHeight; + metadata.Compression = curMetadata.Compression; + metadata.BmpBitsPerPixel = curMetadata.BmpBitsPerPixel; + metadata.HotspotX = curMetadata.HotspotX; + metadata.HotspotY = curMetadata.HotspotY; + metadata.ColorTable = curMetadata.ColorTable; + + break; + case AniFrameFormat.Bmp: + metadata.EncodingWidth = NarrowDimension(size.Width); + metadata.EncodingHeight = NarrowDimension(size.Height); + break; } - return this.currentStream.TryReadUnmanaged(out chunk); + return metadata; + } + + /// + /// Creates decoder options for embedded resources without applying the outer ANI resize twice. + /// + /// The embedded frame decoder options. + private DecoderOptions CreateFrameDecoderOptions() + => new() + { + Configuration = this.Options.Configuration, + MaxFrames = this.Options.MaxFrames, + SkipMetadata = this.Options.SkipMetadata, + SegmentIntegrityHandling = this.Options.SegmentIntegrityHandling, + ColorProfileHandling = this.Options.ColorProfileHandling + }; + + /// + /// Reads one fixed-size RIFF chunk header. + /// + /// The ANI stream. + /// The parsed chunk header. + private AniRiffChunkHeader ReadChunkHeader(BufferedReadStream stream) + { + Span data = this.buffer[..AniConstants.ChunkHeaderSize]; + ReadExactly(stream, data, "RIFF chunk header"); + return AniRiffChunkHeader.Parse(data); } /// - /// Reads the chunk data from the stream. + /// Calculates and validates the exclusive end of a RIFF chunk payload. /// - /// The length of the chunk data to read. - [MethodImpl(InliningOptions.ShortMethod)] - private IMemoryOwner ReadChunkData(uint length) + /// The ANI stream. + /// The declared payload size. + /// The exclusive parent-container boundary. + /// The exclusive payload boundary. + private static long GetChunkDataEnd(BufferedReadStream stream, uint size, long containerEnd) { - if (length is 0) + long end = checked(stream.Position + size); + if (end > containerEnd) { - return new BasicArrayBuffer([]); + throw new InvalidImageContentException("An ANI RIFF chunk extends beyond its containing list."); } - // We rent the buffer here to return it afterwards in Decode() - // We don't want to throw a degenerated memory exception here as we want to allow partial decoding - // so limit the length. - int len = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); - IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(len, AllocationOptions.Clean); + return end; + } - this.currentStream.Read(buffer.GetSpan(), 0, len); + /// + /// Calculates and validates the word-aligned end of a RIFF chunk. + /// + /// The exclusive payload boundary. + /// The declared payload size. + /// The exclusive parent-container boundary. + /// The exclusive padded chunk boundary. + private static long GetPaddedEnd(long dataEnd, uint size, long containerEnd) + { + // RIFF aligns each chunk to a 16-bit boundary without including the optional pad byte in the declared size. + long paddedEnd = dataEnd + (size & 1); + if (paddedEnd > containerEnd) + { + throw new InvalidImageContentException("An ANI RIFF chunk is missing its alignment padding."); + } - return buffer; + return paddedEnd; } - private static byte Narrow(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; + /// + /// Reads an exact number of bytes or reports a truncated ANI file. + /// + /// The ANI stream. + /// The destination buffer. + /// The data description used in the error message. + private static void ReadExactly(BufferedReadStream stream, Span destination, string description) + { + if (stream.Read(destination) != destination.Length) + { + throw new InvalidImageContentException($"Not enough bytes to read the {description}."); + } + } + + /// + /// Converts a pixel dimension to the one-byte ICO/CUR representation. + /// + /// The pixel dimension. + /// The encoded dimension, where zero represents 256 pixels or greater. + private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; } diff --git a/src/ImageSharp/Formats/Ani/AniEncoder.cs b/src/ImageSharp/Formats/Ani/AniEncoder.cs new file mode 100644 index 0000000000..ccc93c3716 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniEncoder.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Encodes images as Windows animated cursors. +/// +public sealed class AniEncoder : QuantizingImageEncoder +{ + /// + /// Initializes a new instance of the class. + /// + public AniEncoder() + { + } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + AniEncoderCore encoder = new(this); + encoder.Encode(image, stream, cancellationToken); + } +} diff --git a/src/ImageSharp/Formats/Ani/AniEncoderCore.cs b/src/ImageSharp/Formats/Ani/AniEncoderCore.cs new file mode 100644 index 0000000000..2b42d8cb55 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniEncoderCore.cs @@ -0,0 +1,468 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Performs ANI encoding. +/// +internal sealed class AniEncoderCore +{ + private readonly AniEncoder encoder; + + // Each nested encoder is configured once and reused for every resource of that type in this ANI operation. + private IcoEncoderCore? icoEncoder; + private CurEncoderCore? curEncoder; + private BmpEncoderCore? bmpEncoder; + + /// + /// Reusable storage for the fixed ANI header and smaller RIFF values. + /// + private InlineArray36 buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + public AniEncoderCore(AniEncoder encoder) + => this.encoder = encoder; + + /// + /// Encodes an image as ANI data. + /// + /// The source pixel type. + /// The source image. + /// The destination stream. + /// The token to monitor for cancellation requests. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); + AniFrameMetadata firstMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); + AniFrameFormat firstFormat = firstMetadata.FrameFormat; + bool bitmapResources = firstFormat is AniFrameFormat.Bmp; + uint displayRate = firstMetadata.FrameDelay is 0 ? imageMetadata.DisplayRate : firstMetadata.FrameDelay; + bool hasVariableRates = false; + int groupCount = 0; + int maxGroupSize = 1; + + // This validation pass derives the fixed ANI header and largest icon directory without allocating a grouping graph. + // Encoding repeats the linear grouping scan below, trading a cheap pass for zero per-group collections. + for (int frameIndex = 0; frameIndex < image.Frames.Count;) + { + AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); + int groupSize = 1; + + // Positive sequence numbers group adjacent resolution variants; non-positive values form independent steps. + if (metadata.SequenceNumber > 0) + { + while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) + { + groupSize++; + } + } + + if (bitmapResources != (metadata.FrameFormat is AniFrameFormat.Bmp)) + { + // AF_ICON applies to the complete file, so raw DIB resources cannot coexist with ICO/CUR resources. + throw new ImageFormatException("ANI cannot mix bitmap resources with ICO or CUR resources."); + } + + if (bitmapResources && groupSize > 1) + { + // Only ICO/CUR directories can contain multiple resolution variants in one physical resource. + throw new ImageFormatException("ANI bitmap resources cannot contain resolution variants."); + } + + // All variants share one animation step, which requires one child format and one rate value. + for (int i = 1; i < groupSize; i++) + { + AniFrameMetadata current = image.Frames[frameIndex + i].Metadata.GetAniMetadata(); + if (current.FrameFormat != metadata.FrameFormat) + { + throw new ImageFormatException("ANI resolution variants must use the same embedded format."); + } + + if (current.FrameDelay != metadata.FrameDelay) + { + throw new ImageFormatException("ANI resolution variants must use the same frame delay."); + } + } + + uint frameDelay = metadata.FrameDelay is 0 ? displayRate : metadata.FrameDelay; + hasVariableRates |= frameDelay != displayRate; + maxGroupSize = Math.Max(maxGroupSize, groupSize); + groupCount++; + frameIndex += groupSize; + } + + // Icon-based ANI files leave global geometry and pixel layout at zero because each ICO/CUR entry owns those values. + AniHeader header = new() + { + BytesInHeader = AniHeader.Size, + FrameCount = (uint)groupCount, + StepCount = (uint)groupCount, + Width = bitmapResources ? imageMetadata.Width is 0 ? (uint)image.Width : imageMetadata.Width : 0, + Height = bitmapResources ? imageMetadata.Height is 0 ? (uint)image.Height : imageMetadata.Height : 0, + BitCount = bitmapResources ? imageMetadata.BitCount is 0 ? 32U : imageMetadata.BitCount : 0, + Planes = bitmapResources ? imageMetadata.Planes is 0 ? 1U : imageMetadata.Planes : 0, + DisplayRate = displayRate, + Flags = bitmapResources ? 0 : AniHeaderFlags.IsIcon + }; + + // One allocator-owned directory buffer is sliced and reused for every icon resource; its capacity is the largest group. + using IMemoryOwner? iconEntriesOwner = bitmapResources ? null : image.Configuration.MemoryAllocator.Allocate(maxGroupSize); + Span iconEntries = iconEntriesOwner is null ? [] : iconEntriesOwner.GetSpan(); + + // ImageEncoder guarantees a seekable destination, allowing direct nested encoding and RIFF size backpatching. + long riffSizePosition = this.BeginContainer(stream, AniConstants.RiffFourCc, AniConstants.AniFormTypeFourCc); + this.WriteHeader(stream, header); + + if (hasVariableRates) + { + this.WriteRates(stream, image, displayRate); + } + + if (!this.encoder.SkipMetadata && (imageMetadata.Name is not null || imageMetadata.Artist is not null)) + { + this.WriteInfoList(stream, imageMetadata, image.Configuration.MemoryAllocator); + } + + long frameListSizePosition = this.BeginContainer(stream, "LIST"u8, "fram"u8); + + // Repeat the allocation-free adjacent grouping scan used by the validation pass. + for (int frameIndex = 0; frameIndex < image.Frames.Count;) + { + cancellationToken.ThrowIfCancellationRequested(); + + AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); + int groupSize = 1; + if (metadata.SequenceNumber > 0) + { + while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) + { + groupSize++; + } + } + + long frameSizePosition = this.BeginChunk(stream, "icon"u8); + this.WriteFrameResource(image, stream, frameIndex, groupSize, metadata.FrameFormat, header.BitCount, iconEntries, cancellationToken); + this.EndChunk(stream, frameSizePosition); + frameIndex += groupSize; + } + + this.EndChunk(stream, frameListSizePosition); + this.EndChunk(stream, riffSizePosition); + } + + /// + /// Writes the fixed-size ANI animation header chunk. + /// + /// The destination stream. + /// The animation header. + private void WriteHeader(Stream stream, AniHeader header) + { + long sizePosition = this.BeginChunk(stream, "anih"u8); + Span data = this.buffer; + header.WriteTo(data); + stream.Write(data); + this.EndChunk(stream, sizePosition); + } + + /// + /// Writes per-step rates when they cannot be represented by one header value. + /// + /// The destination stream. + /// The source image. + /// The default header display rate. + private void WriteRates(Stream stream, Image image, uint displayRate) + { + long sizePosition = this.BeginChunk(stream, "rate"u8); + Span value = this.buffer[..sizeof(uint)]; + + // The rate table contains one DWORD per animation step, not one value per resolution variant. + for (int frameIndex = 0; frameIndex < image.Frames.Count;) + { + AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); + uint frameDelay = metadata.FrameDelay; + BinaryPrimitives.WriteUInt32LittleEndian(value, frameDelay is 0 ? displayRate : frameDelay); + stream.Write(value); + + frameIndex++; + if (metadata.SequenceNumber > 0) + { + while (frameIndex < image.Frames.Count && image.Frames[frameIndex].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) + { + frameIndex++; + } + } + } + + this.EndChunk(stream, sizePosition); + } + + /// + /// Writes the optional ANI name and artist list. + /// + /// The destination stream. + /// The ANI image metadata. + /// The allocator used for the text buffer. + private void WriteInfoList(Stream stream, AniMetadata metadata, MemoryAllocator memoryAllocator) + { + long sizePosition = this.BeginContainer(stream, "LIST"u8, "INFO"u8); + int nameLength = metadata.Name is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Name); + int artistLength = metadata.Artist is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Artist); + + // Name and artist are emitted sequentially, so a single buffer sized for the larger value avoids a second allocation. + using IMemoryOwner owner = memoryAllocator.Allocate(Math.Max(nameLength, artistLength)); + Span buffer = owner.GetSpan(); + + if (metadata.Name is not null) + { + this.WriteTextChunk(stream, "INAM"u8, metadata.Name, buffer); + } + + if (metadata.Artist is not null) + { + this.WriteTextChunk(stream, "IART"u8, metadata.Artist, buffer); + } + + this.EndChunk(stream, sizePosition); + } + + /// + /// Writes a null-terminated ASCII RIFF information chunk. + /// + /// The destination stream. + /// The chunk identifier. + /// The text value. + /// The reusable text buffer. + private void WriteTextChunk(Stream stream, ReadOnlySpan fourCc, string value, Span buffer) + { + long sizePosition = this.BeginChunk(stream, fourCc); + int written = Encoding.ASCII.GetBytes(value, buffer); + stream.Write(buffer[..written]); + + // The terminating zero belongs to the RIFF text payload and is therefore included in the backpatched chunk size. + stream.WriteByte(0); + + this.EndChunk(stream, sizePosition); + } + + /// + /// Encodes one ANI frame resource using the existing ICO, CUR, or BMP encoder. + /// + /// The source pixel type. + /// The source image. + /// The destination stream. + /// The first source-frame index. + /// The number of source frames in this resource. + /// The embedded resource format. + /// The bitmap bit depth declared by the ANI header. + /// The reusable icon directory metadata buffer. + /// The token to monitor for cancellation requests. + private void WriteFrameResource(Image image, Stream stream, int frameIndex, int frameCount, AniFrameFormat format, uint bitCount, Span iconEntries, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + switch (format) + { + case AniFrameFormat.Ico: + case AniFrameFormat.Cur: + // Only the active prefix is exposed to the child encoder; the same backing allocation serves later resources. + Span entries = iconEntries[..frameCount]; + AniIconFrameMetadataProvider provider = new(format); + + if (format is AniFrameFormat.Ico) + { + this.icoEncoder ??= new IcoEncoderCore(new IcoEncoder + { + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + Quantizer = this.encoder.Quantizer, + TransparentColorMode = this.encoder.TransparentColorMode + }); + + this.icoEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); + } + else + { + this.curEncoder ??= new CurEncoderCore(new CurEncoder + { + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + Quantizer = this.encoder.Quantizer, + TransparentColorMode = this.encoder.TransparentColorMode + }); + + this.curEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); + } + + break; + case AniFrameFormat.Bmp: + if (this.bmpEncoder is null) + { + BmpEncoder bmpEncoder = new() + { + BitsPerPixel = GetBmpBitsPerPixel(bitCount), + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + Quantizer = this.encoder.Quantizer, + SkipFileHeader = true, + SupportTransparency = bitCount is 32, + TransparentColorMode = this.encoder.TransparentColorMode + }; + + this.bmpEncoder = new BmpEncoderCore(bmpEncoder, image.Configuration.MemoryAllocator); + } + + // The frame overload writes the raw DIB directly and avoids constructing a temporary single-frame Image. + this.bmpEncoder.Encode(image.Frames[frameIndex], image.Metadata, stream, cancellationToken); + break; + } + } + + /// + /// Creates an icon directory entry from ANI-owned frame metadata. + /// + /// The ANI frame metadata. + /// The embedded icon format. + /// The source frame size. + /// The icon directory entry. + private static IconDirEntry CreateIconDirEntry(AniFrameMetadata metadata, AniFrameFormat format, Size size) + { + // PNG and direct-color bitmap entries do not declare a palette; indexed bitmap entries advertise their color count. + byte colorCount = metadata.Compression is IconFrameCompression.Png || metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 + ? (byte)0 + : (byte)ColorNumerics.GetColorCountForBitDepth((int)metadata.BmpBitsPerPixel); + + // ICO stores planes/BPP in these fields, while CUR reuses the same two words for the hotspot coordinates. + return new IconDirEntry + { + Width = metadata.EncodingWidth ?? NarrowDimension(size.Width), + Height = metadata.EncodingHeight ?? NarrowDimension(size.Height), + ColorCount = colorCount, + Planes = format is AniFrameFormat.Ico ? (ushort)1 : metadata.HotspotX, + BitCount = format is AniFrameFormat.Ico + ? metadata.Compression is IconFrameCompression.Bmp ? (ushort)metadata.BmpBitsPerPixel : (ushort)32 + : metadata.HotspotY + }; + } + + /// + /// Converts an ANI bitmap bit depth to a supported BMP encoder value. + /// + /// The ANI bit depth. + /// The BMP encoder bit depth. + private static BmpBitsPerPixel GetBmpBitsPerPixel(uint bitCount) + => bitCount switch + { + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + 4 => BmpBitsPerPixel.Bit4, + 8 => BmpBitsPerPixel.Bit8, + 16 => BmpBitsPerPixel.Bit16, + 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 + }; + + /// + /// Converts a pixel dimension to the one-byte ICO/CUR representation. + /// + /// The pixel dimension. + /// The encoded dimension, where zero represents 256 pixels or greater. + private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; + + /// + /// Begins a RIFF chunk whose size will be backpatched after its payload is written. + /// + /// The destination stream. + /// The chunk identifier. + /// The stream position of the chunk-size field. + private long BeginChunk(Stream stream, ReadOnlySpan fourCc) + { + stream.Write(fourCc); + long sizePosition = stream.Position; + + // Payload length is unknown until nested encoding completes, so reserve the DWORD and remember its absolute position. + Span size = this.buffer[..sizeof(uint)]; + size.Clear(); + stream.Write(size); + + return sizePosition; + } + + /// + /// Begins a RIFF container chunk and writes its form or list type. + /// + /// The destination stream. + /// The container identifier. + /// The container form or list type. + /// The stream position of the container-size field. + private long BeginContainer(Stream stream, ReadOnlySpan fourCc, ReadOnlySpan type) + { + long sizePosition = this.BeginChunk(stream, fourCc); + stream.Write(type); + return sizePosition; + } + + /// + /// Word-aligns a RIFF chunk and writes its payload size into the reserved field. + /// + /// The destination stream. + /// The stream position of the reserved size field. + private void EndChunk(Stream stream, long sizePosition) + { + long endPosition = stream.Position; + + // sizePosition addresses the size DWORD itself; subtracting its four bytes yields payload length. + uint dataSize = checked((uint)(endPosition - sizePosition - sizeof(uint))); + + // RIFF chunk sizes exclude the optional padding byte used to align the next chunk to a WORD boundary. + if ((dataSize & 1) is 1) + { + stream.WriteByte(0); + endPosition++; + } + + Span size = this.buffer[..sizeof(uint)]; + BinaryPrimitives.WriteUInt32LittleEndian(size, dataSize); + + // Backpatch only the reserved DWORD, then restore the append position after any alignment byte. + stream.Position = sizePosition; + stream.Write(size); + stream.Position = endPosition; + } + + /// + /// Projects ANI-owned metadata into the icon encoder without allocating intermediary metadata objects. + /// + private readonly struct AniIconFrameMetadataProvider : IconEncoderCore.IEncodingFrameMetadataProvider + { + private readonly AniFrameFormat format; + + /// + /// Initializes a new instance of the struct. + /// + /// The embedded icon format. + public AniIconFrameMetadataProvider(AniFrameFormat format) + => this.format = format; + + /// + public IconEncoderCore.EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory? colorTable) + { + AniFrameMetadata metadata = frame.Metadata.GetAniMetadata(); + colorTable = metadata.ColorTable; + return new IconEncoderCore.EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, CreateIconDirEntry(metadata, this.format, frame.Size)); + } + } +} diff --git a/src/ImageSharp/Formats/Ani/AniFormat.cs b/src/ImageSharp/Formats/Ani/AniFormat.cs index cc13e1aae8..04781974ec 100644 --- a/src/ImageSharp/Formats/Ani/AniFormat.cs +++ b/src/ImageSharp/Formats/Ani/AniFormat.cs @@ -4,10 +4,17 @@ namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Registers the image encoders, decoders and mime type detectors for the bmp format. +/// Describes the ANI image format. /// public sealed class AniFormat : IImageFormat { + /// + /// Prevents a default instance of the class from being created. + /// + private AniFormat() + { + } + /// /// Gets the shared instance. /// diff --git a/src/ImageSharp/Formats/Ani/AniFrameFormat.cs b/src/ImageSharp/Formats/Ani/AniFrameFormat.cs index 661aae7fc3..c7902ee1c5 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameFormat.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameFormat.cs @@ -6,20 +6,20 @@ namespace SixLabors.ImageSharp.Formats.Ani; /// /// Specifies the format of the frame data. /// -public enum AniFrameFormat +public enum AniFrameFormat : byte { /// - /// The frame data is in ICO format. + /// The frame resource is encoded as a Windows cursor. /// - Ico = 1, + Cur, /// - /// The frame data is in CUR format. + /// The frame resource is encoded as a Windows icon. /// - Cur, + Ico, /// - /// The frame data is in BMP format. + /// The frame resource is encoded as a Windows bitmap. /// Bmp } diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs index 5af0b6fc7c..4fe31788c8 100644 --- a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -2,14 +2,14 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Formats.Cur; -using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Provides Ani specific metadata information for the image. +/// Provides ANI-specific metadata for an image frame. /// public class AniFrameMetadata : IFormatFrameMetadata { @@ -21,77 +21,228 @@ public AniFrameMetadata() } /// - /// Gets or sets the display time for this frame (in 1/60 seconds) + /// Initializes a new instance of the class by copying another instance. + /// + /// The metadata to copy. + private AniFrameMetadata(AniFrameMetadata other) + { + this.FrameDelay = other.FrameDelay; + this.SequenceNumber = other.SequenceNumber; + this.EncodingWidth = other.EncodingWidth; + this.EncodingHeight = other.EncodingHeight; + this.FrameFormat = other.FrameFormat; + this.Compression = other.Compression; + this.BmpBitsPerPixel = other.BmpBitsPerPixel; + this.HotspotX = other.HotspotX; + this.HotspotY = other.HotspotY; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + } + + /// + /// Gets or sets the frame display time in sixtieths of a second. /// public uint FrameDelay { get; set; } /// - /// Gets or sets the sequence number of current frame. + /// Gets or sets the animation sequence number. + /// Adjacent frames with the same positive value are grouped as resolution variants in one ANI frame resource. + /// A non-positive value encodes the frame as its own animation step. /// - public int SequenceNumber { get; set; } = 1; + public int SequenceNumber { get; set; } /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded frame width. + /// A value of zero represents 256 pixels or greater in ICO and CUR resources. ///
public byte? EncodingWidth { get; set; } /// - /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded frame height. + /// A value of zero represents 256 pixels or greater in ICO and CUR resources. ///
public byte? EncodingHeight { get; set; } /// - /// Gets or sets a value indicating whether the frame will be encoded as an ICO or CUR or BMP file. + /// Gets or sets the format used for this frame resource. /// public AniFrameFormat FrameFormat { get; set; } /// - /// Gets or sets the of one "icon" chunk. + /// Gets or sets the embedded ICO or CUR compression format. /// - public IcoFrameMetadata? IcoFrameMetadata { get; set; } + public IconFrameCompression Compression { get; set; } = IconFrameCompression.Png; /// - /// Gets or sets the of one "icon" chunk. + /// Gets or sets the embedded bitmap bits per pixel. /// - public CurFrameMetadata? CurFrameMetadata { get; set; } + public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; + + /// + /// Gets or sets the embedded bitmap color table. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? ColorTable { get; set; } + + /// + /// Gets or sets the horizontal cursor hotspot in pixels from the left. + /// + public ushort HotspotX { get; set; } + + /// + /// Gets or sets the vertical cursor hotspot in pixels from the top. + /// + public ushort HotspotY { get; set; } /// - public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => - new() + public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + { + int bitsPerPixel = metadata.PixelTypeInfo?.BitsPerPixel ?? 32; + BmpBitsPerPixel bmpBitsPerPixel = bitsPerPixel switch { - FrameDelay = (uint)metadata.Duration.TotalSeconds * 60 + 1 => BmpBitsPerPixel.Bit1, + 2 => BmpBitsPerPixel.Bit2, + <= 4 => BmpBitsPerPixel.Bit4, + <= 8 => BmpBitsPerPixel.Bit8, + <= 16 => BmpBitsPerPixel.Bit16, + <= 24 => BmpBitsPerPixel.Bit24, + _ => BmpBitsPerPixel.Bit32 }; - /// - IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + return new AniFrameMetadata + { + FrameDelay = (uint)Math.Round(metadata.Duration.TotalSeconds * 60), + EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), + Compression = bmpBitsPerPixel is BmpBitsPerPixel.Bit32 ? IconFrameCompression.Png : IconFrameCompression.Bmp, + BmpBitsPerPixel = bmpBitsPerPixel + }; + } /// public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() - { - // TODO: Implement. You need to consider encoding width/height. - return new() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) }; - } + => new() + { + Duration = TimeSpan.FromSeconds(this.FrameDelay / 60D), + EncodingWidth = this.EncodingWidth, + EncodingHeight = this.EncodingHeight, + PixelTypeInfo = this.GetPixelTypeInfo() + }; /// public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel { - // TODO: Implement. You need to consider encoding width/height. + float ratioX = destination.Width / (float)source.Width; + float ratioY = destination.Height / (float)source.Height; + + this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX); + this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY); + this.ColorTable = null; } /// - public AniFrameMetadata DeepClone() => new() + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public AniFrameMetadata DeepClone() => new(this); + + /// + /// Gets the pixel layout represented by the embedded resource metadata. + /// + /// The represented pixel layout. + private PixelTypeInfo GetPixelTypeInfo() { - FrameDelay = this.FrameDelay, - EncodingHeight = this.EncodingHeight, - EncodingWidth = this.EncodingWidth, - SequenceNumber = this.SequenceNumber, - FrameFormat = this.FrameFormat, - IcoFrameMetadata = this.IcoFrameMetadata?.DeepClone(), - CurFrameMetadata = this.CurFrameMetadata?.DeepClone(), - - // TODO SubImageMetadata - }; + int bitsPerPixel = (int)this.BmpBitsPerPixel; + PixelComponentInfo componentInfo; + PixelColorType colorType; + PixelAlphaRepresentation alphaRepresentation = PixelAlphaRepresentation.None; + + if (this.Compression is IconFrameCompression.Png) + { + bitsPerPixel = 32; + componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8); + colorType = PixelColorType.RGB | PixelColorType.Alpha; + alphaRepresentation = PixelAlphaRepresentation.Unassociated; + } + else + { + switch (this.BmpBitsPerPixel) + { + case BmpBitsPerPixel.Bit1: + componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 1); + colorType = PixelColorType.Binary; + break; + case BmpBitsPerPixel.Bit2: + componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 2); + colorType = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit4: + componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 4); + colorType = PixelColorType.Indexed; + break; + case BmpBitsPerPixel.Bit8: + componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 8); + colorType = PixelColorType.Indexed; + break; + + // Windows bitmaps commonly use a 5-6-5 layout for 16-bit color. + case BmpBitsPerPixel.Bit16: + componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 5, 6, 5); + colorType = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit24: + componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 8, 8, 8); + colorType = PixelColorType.RGB; + break; + case BmpBitsPerPixel.Bit32 or _: + componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8); + colorType = PixelColorType.RGB | PixelColorType.Alpha; + alphaRepresentation = PixelAlphaRepresentation.Unassociated; + break; + } + } + + return new PixelTypeInfo(bitsPerPixel) + { + AlphaRepresentation = alphaRepresentation, + ComponentInfo = componentInfo, + ColorType = colorType + }; + } + + /// + /// Scales an encoded dimension after an image transform. + /// + /// The encoded source dimension. + /// The full destination dimension. + /// The destination-to-source scale ratio. + /// The encoded destination dimension. + private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) + { + if (value is null) + { + return ClampEncodingDimension(destination); + } + + // ICO and CUR encode dimensions in one byte, where zero represents 256 pixels or greater. + int source = value.Value is 0 ? 256 : value.Value; + return ClampEncodingDimension(MathF.Ceiling(source * ratio)); + } + + /// + /// Converts a pixel dimension to the one-byte ICO/CUR representation. + /// + /// The pixel dimension. + /// The encoded dimension. + private static byte ClampEncodingDimension(float? dimension) + => dimension switch + { + > 255 => 0, + >= 1 => (byte)dimension, + _ => 0 + }; } diff --git a/src/ImageSharp/Formats/Ani/AniFrameStream.cs b/src/ImageSharp/Formats/Ani/AniFrameStream.cs new file mode 100644 index 0000000000..7a0b9ca3c8 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniFrameStream.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Exposes one ANI frame-resource chunk as an isolated seekable stream. +/// +/// +/// Embedded decoders accept arbitrary seek offsets from their own headers. Bounding those seeks to the +/// current RIFF chunk prevents malformed ICO, CUR, or BMP offsets from reading neighboring ANI chunks. +/// +internal sealed class AniFrameStream : Stream +{ + private readonly Stream stream; + + // start is absolute in the containing stream; position is always relative to this bounded resource. + private long start; + private long length; + private long position; + + /// + /// Initializes a new instance of the class. + /// + /// The containing ANI stream. + public AniFrameStream(Stream stream) + => this.stream = stream; + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => true; + + /// + public override bool CanWrite => false; + + /// + public override long Length => this.length; + + /// + public override long Position + { + get => this.position; + set => this.Seek(value, SeekOrigin.Begin); + } + + /// + /// Repositions this stream over another frame-resource payload in the same containing stream. + /// + /// The absolute start of the frame-resource payload. + /// The frame-resource payload length. + public void Reset(long start, long length) + { + this.start = start; + this.length = length; + this.position = 0; + } + + /// + public override void Flush() + { + } + + /// + public override int Read(byte[] buffer, int offset, int count) + => this.Read(buffer.AsSpan(offset, count)); + + /// + public override int Read(Span buffer) + { + // Clamp every read to the resource boundary so a child decoder cannot consume the next RIFF chunk. + int count = (int)Math.Min(buffer.Length, this.length - this.position); + if (count is 0) + { + return 0; + } + + // The containing stream is shared by all resources, so synchronize its absolute position immediately before reading. + this.stream.Position = this.start + this.position; + int read = this.stream.Read(buffer[..count]); + this.position += read; + + return read; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + long target = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.position + offset, + SeekOrigin.End => this.length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; + + // Casting rejects both negative offsets and offsets beyond Length with one bounds check. + if ((ulong)target > (ulong)this.length) + { + throw new InvalidImageContentException("The embedded ANI frame resource contains an invalid seek offset."); + } + + // Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resource processing. + this.position = target; + return target; + } + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 1656911cc4..e886df6b6c 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -1,32 +1,98 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Buffers.Binary; namespace SixLabors.ImageSharp.Formats.Ani; -internal readonly struct AniHeader +/// +/// Represents the data stored in an ANI "anih" chunk. +/// +internal struct AniHeader { - public uint Size { get; } + /// + /// The number of bytes in the ANI header. + /// + public const int Size = 9 * sizeof(uint); - public uint Frames { get; } + /// + /// Gets or sets the declared ANI header size. + /// + public uint BytesInHeader { get; set; } - public uint Steps { get; } + /// + /// Gets or sets the number of embedded frame resources. + /// + public uint FrameCount { get; set; } - public uint Width { get; } + /// + /// Gets or sets the number of animation steps. + /// + public uint StepCount { get; set; } - public uint Height { get; } + /// + /// Gets or sets the frame width used by bitmap-based animations. + /// + public uint Width { get; set; } - public uint BitCount { get; } + /// + /// Gets or sets the frame height used by bitmap-based animations. + /// + public uint Height { get; set; } - public uint Planes { get; } + /// + /// Gets or sets the encoded bits per pixel. + /// + public uint BitCount { get; set; } - public uint DisplayRate { get; } + /// + /// Gets or sets the number of color planes. + /// + public uint Planes { get; set; } - public AniHeaderFlags Flags { get; } + /// + /// Gets or sets the default display rate in sixtieths of a second. + /// + public uint DisplayRate { get; set; } - public static ref AniHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + /// + /// Gets or sets the ANI header flags. + /// + public AniHeaderFlags Flags { get; set; } - public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); + /// + /// Parses an ANI header from its little-endian byte representation. + /// + /// The ANI header data. + /// The parsed ANI header. + public static AniHeader Parse(ReadOnlySpan data) + => new() + { + BytesInHeader = BinaryPrimitives.ReadUInt32LittleEndian(data), + FrameCount = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]), + StepCount = BinaryPrimitives.ReadUInt32LittleEndian(data[8..]), + Width = BinaryPrimitives.ReadUInt32LittleEndian(data[12..]), + Height = BinaryPrimitives.ReadUInt32LittleEndian(data[16..]), + BitCount = BinaryPrimitives.ReadUInt32LittleEndian(data[20..]), + Planes = BinaryPrimitives.ReadUInt32LittleEndian(data[24..]), + DisplayRate = BinaryPrimitives.ReadUInt32LittleEndian(data[28..]), + Flags = (AniHeaderFlags)BinaryPrimitives.ReadUInt32LittleEndian(data[32..]) + }; + + /// + /// Writes the ANI header to its little-endian byte representation. + /// + /// The destination buffer. + public readonly void WriteTo(Span destination) + { + BinaryPrimitives.WriteUInt32LittleEndian(destination, this.BytesInHeader); + BinaryPrimitives.WriteUInt32LittleEndian(destination[4..], this.FrameCount); + BinaryPrimitives.WriteUInt32LittleEndian(destination[8..], this.StepCount); + BinaryPrimitives.WriteUInt32LittleEndian(destination[12..], this.Width); + BinaryPrimitives.WriteUInt32LittleEndian(destination[16..], this.Height); + BinaryPrimitives.WriteUInt32LittleEndian(destination[20..], this.BitCount); + BinaryPrimitives.WriteUInt32LittleEndian(destination[24..], this.Planes); + BinaryPrimitives.WriteUInt32LittleEndian(destination[28..], this.DisplayRate); + BinaryPrimitives.WriteUInt32LittleEndian(destination[32..], (uint)this.Flags); + } } diff --git a/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs b/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs index 37a27c28b3..528a8908fc 100644 --- a/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs +++ b/src/ImageSharp/Formats/Ani/AniHeaderFlags.cs @@ -10,12 +10,12 @@ namespace SixLabors.ImageSharp.Formats.Ani; public enum AniHeaderFlags : uint { /// - /// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file. + /// The "icon" chunks contain ICO or CUR resources. Without this flag, they contain BMP resources. /// IsIcon = 1, /// - /// If set, the ANI file contains a "seq " chunk. + /// The ANI file contains a "seq " chunk that maps animation steps to frame resources. /// - ContainsSeq = 2 + ContainsSequence = 2 } diff --git a/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs index 83826da111..5b63a28183 100644 --- a/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs @@ -2,17 +2,23 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Webp; namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Detects ico file headers. +/// Detects ANI file headers. /// -public class AniImageFormatDetector : IImageFormatDetector +public sealed class AniImageFormatDetector : IImageFormatDetector { + /// + /// Initializes a new instance of the class. + /// + public AniImageFormatDetector() + { + } + /// - public int HeaderSize => RiffOrListChunkHeader.HeaderSize; + public int HeaderSize => AniConstants.RiffHeaderSize; /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) @@ -21,10 +27,13 @@ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out I return format is not null; } + /// + /// Determines whether the supplied header is a RIFF container with the ANI "ACON" form type. + /// + /// The candidate file header. + /// when the header identifies ANI data. private bool IsSupportedFileFormat(ReadOnlySpan header) - => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is - { - IsRiff: true, - FormType: AniConstants.AniFourCc - }; + => header.Length >= this.HeaderSize + && header[..4].SequenceEqual(AniConstants.RiffFourCc) + && header.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc); } diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index 9b09cfa7a7..c66dc17410 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Formats.Ani; /// -/// Provides Ani specific metadata information for the image. +/// Provides ANI-specific metadata for an image. /// public class AniMetadata : IFormatMetadata { @@ -19,90 +19,116 @@ public AniMetadata() } /// - /// Gets or sets the width of frames in the animation. + /// Initializes a new instance of the class by copying another instance. + /// + /// The metadata to copy. + private AniMetadata(AniMetadata other) + { + this.Width = other.Width; + this.Height = other.Height; + this.BitCount = other.BitCount; + this.Planes = other.Planes; + this.DisplayRate = other.DisplayRate; + this.Flags = other.Flags; + this.Name = other.Name; + this.Artist = other.Artist; + } + + /// + /// Gets or sets the frame width declared by the ANI header. /// /// - /// Remains zero when has flag + /// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions. /// public uint Width { get; set; } /// - /// Gets or sets the height of frames in the animation. + /// Gets or sets the frame height declared by the ANI header. /// /// - /// Remains zero when has flag + /// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions. /// public uint Height { get; set; } /// - /// Gets or sets the number of bits per pixel. + /// Gets or sets the bits per pixel declared by the ANI header. /// /// - /// Remains zero when has flag + /// Bitmap-based ANI files use this value to describe their raw frame data. Icon-based files commonly store zero + /// because each embedded ICO or CUR entry declares its own pixel layout. /// public uint BitCount { get; set; } /// - /// Gets or sets the number of frames in the animation. + /// Gets or sets the color-plane count declared by the ANI header. /// /// - /// Remains zero when has flag + /// Bitmap-based ANI files use one plane. Icon-based files commonly store zero because this header field is reserved + /// when the embedded resources are ICO or CUR data. /// public uint Planes { get; set; } /// - /// Gets or sets the default display rate of frames in the animation. + /// Gets or sets the default frame display rate in sixtieths of a second. /// public uint DisplayRate { get; set; } /// - /// Gets or sets the flags for the ANI header. + /// Gets or sets the ANI header flags. /// - public AniHeaderFlags Flags { get; set; } + public AniHeaderFlags Flags { get; set; } = AniHeaderFlags.IsIcon; /// - /// Gets or sets the name of the ANI file. + /// Gets or sets the animation name. /// public string? Name { get; set; } /// - /// Gets or sets the artist of the ANI file. + /// Gets or sets the animation artist. /// public string? Artist { get; set; } /// public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) - => throw new NotImplementedException(); + => new() + { + BitCount = (uint)metadata.PixelTypeInfo.BitsPerPixel, + Planes = 1, + Flags = AniHeaderFlags.IsIcon + }; /// - public void AfterImageApply(Image destination, Matrix4x4 matrix) - where TPixel : unmanaged, IPixel + public PixelTypeInfo GetPixelTypeInfo() { + // Icon-based files are allowed to leave the global bit depth unspecified. Their embedded + // ICO/CUR metadata carries the exact value, while 32-bit is the least lossy conversion default. + int bitsPerPixel = this.BitCount is > 0 and <= 32 ? (int)this.BitCount : 32; + return new PixelTypeInfo(bitsPerPixel); } /// - IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + AnimateRootFrame = true, + EncodingType = EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo() + }; /// - public PixelTypeInfo GetPixelTypeInfo() - => throw new NotImplementedException(); + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + if (!this.Flags.HasFlag(AniHeaderFlags.IsIcon)) + { + this.Width = (uint)destination.Width; + this.Height = (uint)destination.Height; + } + } /// - public FormatConnectingMetadata ToFormatConnectingMetadata() - => throw new NotImplementedException(); + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); /// - public AniMetadata DeepClone() => new() - { - Width = this.Width, - Height = this.Height, - BitCount = this.BitCount, - Planes = this.Planes, - DisplayRate = this.DisplayRate, - Flags = this.Flags, - Name = this.Name, - Artist = this.Artist - - // TODO IconFrames - }; + public AniMetadata DeepClone() => new(this); } diff --git a/src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs b/src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs new file mode 100644 index 0000000000..0968741793 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Represents a RIFF chunk identifier and payload size. +/// +internal struct AniRiffChunkHeader +{ + /// + /// Gets or sets the chunk identifier. + /// + public uint FourCc { get; set; } + + /// + /// Gets or sets the chunk payload size in bytes, excluding alignment padding. + /// + public uint Size { get; set; } + + /// + /// Parses a RIFF chunk header from its little-endian byte representation. + /// + /// The RIFF chunk header data. + /// The parsed RIFF chunk header. + public static AniRiffChunkHeader Parse(ReadOnlySpan data) + => new() + { + FourCc = BinaryPrimitives.ReadUInt32LittleEndian(data), + Size = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]) + }; +} diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 0bf57c5612..f543e4f3dc 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -138,17 +138,30 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - // Stream may not at 0. + this.Encode(image.Frames.RootFrame, image.Metadata, stream, cancellationToken); + } + + /// + /// Encodes a source frame using the supplied image metadata. + /// + /// The pixel format. + /// The source frame. + /// The source image metadata. + /// The destination stream. + /// The token to request cancellation. + internal void Encode(ImageFrame frame, ImageMetadata metadata, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + // Nested ANI/ICO/CUR encoding starts inside a parent stream, so all later profile offsets use this local base. long basePosition = stream.Position; - Configuration configuration = image.Configuration; - ImageMetadata metadata = image.Metadata; + Configuration configuration = frame.Configuration; BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; ushort bpp = (ushort)this.bitsPerPixel; - int bytesPerLine = (int)(4 * ((((uint)image.Width * bpp) + 31) / 32)); - this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F)); + int bytesPerLine = (int)(4 * ((((uint)frame.Width * bpp) + 31) / 32)); + this.padding = bytesPerLine - (int)(frame.Width * (bpp / 8F)); int colorPaletteSize = this.bitsPerPixel switch { @@ -176,25 +189,25 @@ public void Encode(Image image, Stream stream, CancellationToken _ => BmpInfoHeader.SizeV3 }; - // for ico/cur encoder. - int height = image.Height; + // ICO/CUR DIB headers include the XOR bitmap and following AND mask in one doubled height. + int height = frame.Height; if (this.isDoubleHeight) { height <<= 1; } - BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData); + BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(frame.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData); Span buffer = stackalloc byte[infoHeaderSize]; - // For ico/cur encoder. + // ICO/CUR resources contain a DIB directly; standalone BMP files additionally require BITMAPFILEHEADER. if (!this.skipFileHeader) { WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); } this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); - this.WriteImage(configuration, stream, image, cancellationToken); + this.WriteImage(configuration, stream, frame, cancellationToken); WriteColorProfile(stream, iccProfileData, buffer, basePosition); stream.Flush(); @@ -348,15 +361,11 @@ private void WriteBitmapInfoHeader(Stream stream, BmpInfoHeader infoHeader, Span /// The pixel format. /// The global configuration. /// The to write to. - /// + /// /// The containing pixel data. /// /// The token to monitor for cancellation requests. - private void WriteImage( - Configuration configuration, - Stream stream, - Image image, - CancellationToken cancellationToken) + private void WriteImage(Configuration configuration, Stream stream, ImageFrame frame, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { ImageFrame? clonedFrame = null; @@ -367,11 +376,11 @@ private void WriteImage( int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32; if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels(this.transparentColorMode)) { - clonedFrame = image.Frames.RootFrame.Clone(); + clonedFrame = frame.Clone(); EncodingUtilities.ReplaceTransparentPixels(clonedFrame); } - ImageFrame encodingFrame = clonedFrame ?? image.Frames.RootFrame; + ImageFrame encodingFrame = clonedFrame ?? frame; Buffer2D pixels = encodingFrame.PixelBuffer; switch (this.bitsPerPixel) @@ -864,9 +873,16 @@ private static void Write1BitPalette(Stream stream, int startIdx, int endIdx, Re stream.WriteByte(indices); } + /// + /// Writes the bottom-up 1-bit transparency mask required by an ICO/CUR bitmap resource. + /// + /// The source pixel type. + /// The destination stream. + /// The source frame. private static void ProcessedAlphaMask(Stream stream, ImageFrame encodingFrame) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { + // Each byte represents eight pixels and every scanline is padded to a 4-byte DIB boundary. int arrayWidth = encodingFrame.Width / 8; int padding = arrayWidth % 4; if (padding is not 0) @@ -875,6 +891,9 @@ private static void ProcessedAlphaMask(Stream stream, ImageFrame } Span mask = stackalloc byte[arrayWidth]; + Span paddingBytes = stackalloc byte[3]; + paddingBytes.Clear(); + for (int y = encodingFrame.Height - 1; y >= 0; y--) { mask.Clear(); @@ -891,12 +910,21 @@ private static void ProcessedAlphaMask(Stream stream, ImageFrame } stream.Write(mask); - stream.Skip(padding); + + // Alpha-mask rows are DWORD-aligned, and the final row padding must extend the stream. + stream.Write(paddingBytes[..padding]); } } + /// + /// Sets one most-significant-bit-first transparency flag in an ICO/CUR AND-mask byte. + /// + /// The source pixel type. + /// The source pixel. + /// The destination mask byte. + /// The pixel index within the byte. private static void WriteAlphaMask(in TPixel pixel, ref byte mask, in int index) - where TPixel : unmanaged, IPixel + where TPixel : unmanaged, IPixel { Rgba32 rgba = pixel.ToRgba32(); if (rgba.A is 0) diff --git a/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs index 879b3f1123..ba092ebc7f 100644 --- a/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs +++ b/src/ImageSharp/Formats/Cur/CurConfigurationModule.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Icon; - namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Registers the image encoders, decoders and mime type detectors for the Ico format. +/// Registers the image encoder, decoder, and format detector for the CUR format. /// public sealed class CurConfigurationModule : IImageFormatConfigurationModule { @@ -15,6 +13,6 @@ public void Configure(Configuration configuration) { configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder()); configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance); - configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); + configuration.ImageFormatsManager.AddImageFormatDetector(new CurImageFormatDetector()); } } diff --git a/src/ImageSharp/Formats/Cur/CurConstants.cs b/src/ImageSharp/Formats/Cur/CurConstants.cs index 7abf4c812e..3edd3137ac 100644 --- a/src/ImageSharp/Formats/Cur/CurConstants.cs +++ b/src/ImageSharp/Formats/Cur/CurConstants.cs @@ -4,12 +4,12 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Defines constants relating to ICOs +/// Defines constants used by the CUR format. /// internal static class CurConstants { /// - /// The list of mime types that equate to a cur. + /// The MIME types that identify CUR data. /// /// /// See @@ -27,13 +27,11 @@ internal static class CurConstants "image/ico", "image/icon", "text/ico", - "application/ico", + "application/ico" ]; /// - /// The list of file extensions that equate to a cur. + /// The file extensions that identify CUR data. /// public static readonly IEnumerable FileExtensions = ["cur"]; - - public const uint FileHeader = 0x00_02_00_00; } diff --git a/src/ImageSharp/Formats/Cur/CurDecoder.cs b/src/ImageSharp/Formats/Cur/CurDecoder.cs index cbe646c47b..522e91be56 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoder.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoder.cs @@ -6,10 +6,13 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Decoder for generating an image out of a ico encoded stream. +/// Decoder for generating an image from a CUR encoded stream. /// public sealed class CurDecoder : ImageDecoder { + /// + /// Initializes a new instance of the class. + /// private CurDecoder() { } @@ -34,7 +37,7 @@ protected override Image Decode(DecoderOptions options, Stream s /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); + => this.Decode(options, stream, cancellationToken); /// protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs index 6fc8905279..9471b79580 100644 --- a/src/ImageSharp/Formats/Cur/CurDecoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurDecoderCore.cs @@ -7,13 +7,21 @@ namespace SixLabors.ImageSharp.Formats.Cur; +/// +/// Decodes CUR containers and maps directory metadata to CUR metadata. +/// internal sealed class CurDecoderCore : IconDecoderCore { + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. public CurDecoderCore(DecoderOptions options) - : base(options) + : base(options, IconFileType.CUR) { } + /// protected override void SetFrameMetadata( ImageMetadata imageMetadata, ImageFrameMetadata frameMetadata, diff --git a/src/ImageSharp/Formats/Cur/CurEncoder.cs b/src/ImageSharp/Formats/Cur/CurEncoder.cs index e19a73990c..f4a0369b3b 100644 --- a/src/ImageSharp/Formats/Cur/CurEncoder.cs +++ b/src/ImageSharp/Formats/Cur/CurEncoder.cs @@ -10,8 +10,5 @@ public sealed class CurEncoder : QuantizingImageEncoder { /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) - { - CurEncoderCore encoderCore = new(this); - encoderCore.Encode(image, stream, cancellationToken); - } + => new CurEncoderCore(this).Encode(image, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs index 6435587e2f..b38e89214e 100644 --- a/src/ImageSharp/Formats/Cur/CurEncoderCore.cs +++ b/src/ImageSharp/Formats/Cur/CurEncoderCore.cs @@ -2,13 +2,46 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Cur; +/// +/// Encodes CUR containers using CUR frame metadata. +/// internal sealed class CurEncoderCore : IconEncoderCore { + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. public CurEncoderCore(QuantizingImageEncoder encoder) : base(encoder, IconFileType.CUR) { } + + /// + /// Encodes all source frames as a CUR resource. + /// + /// The source pixel type. + /// The source image. + /// The destination stream. + /// The token to monitor for cancellation requests. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.Encode(image, stream, default(CurFrameMetadataProvider), cancellationToken); + + /// + /// Supplies CUR frame metadata to the shared container encoder. + /// + private readonly struct CurFrameMetadataProvider : IEncodingFrameMetadataProvider + { + /// + public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory? colorTable) + { + CurFrameMetadata metadata = frame.Metadata.GetCurMetadata(); + colorTable = metadata.ColorTable; + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry(frame.Size)); + } + } } diff --git a/src/ImageSharp/Formats/Cur/CurFormat.cs b/src/ImageSharp/Formats/Cur/CurFormat.cs index af93382ec0..a4bd959898 100644 --- a/src/ImageSharp/Formats/Cur/CurFormat.cs +++ b/src/ImageSharp/Formats/Cur/CurFormat.cs @@ -4,10 +4,13 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Registers the image encoders, decoders and mime type detectors for the ICO format. +/// Describes the CUR image format. /// public sealed class CurFormat : IImageFormat { + /// + /// Prevents a default instance of the class from being created. + /// private CurFormat() { } @@ -18,10 +21,10 @@ private CurFormat() public static CurFormat Instance { get; } = new(); /// - public string Name => "ICO"; + public string Name => "CUR"; /// - public string DefaultMimeType => CurConstants.MimeTypes.First(); + public string DefaultMimeType => "image/vnd.microsoft.icon"; /// public IEnumerable MimeTypes => CurConstants.MimeTypes; diff --git a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs index dbe8183b6d..806ebf039a 100644 --- a/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurFrameMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// -/// IcoFrameMetadata. +/// Provides CUR-specific metadata for an image frame. /// public class CurFrameMetadata : IFormatFrameMetadata { @@ -20,6 +20,10 @@ public CurFrameMetadata() { } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The metadata to copy. private CurFrameMetadata(CurFrameMetadata other) { this.Compression = other.Compression; @@ -28,10 +32,15 @@ private CurFrameMetadata(CurFrameMetadata other) this.EncodingWidth = other.EncodingWidth; this.EncodingHeight = other.EncodingHeight; this.BmpBitsPerPixel = other.BmpBitsPerPixel; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } } /// - /// Gets or sets the frame compressions format. + /// Gets or sets the frame compression format. /// public IconFrameCompression Compression { get; set; } @@ -46,14 +55,14 @@ private CurFrameMetadata(CurFrameMetadata other) public ushort HotspotY { get; set; } /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded width. + /// A value of zero represents 256 pixels or greater. ///
public byte? EncodingWidth { get; set; } /// - /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded height. + /// A value of zero represents 256 pixels or greater. ///
public byte? EncodingHeight { get; set; } @@ -104,7 +113,7 @@ public static CurFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectin BmpBitsPerPixel = bbpp, Compression = compression, EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), - EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), + EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight) }; } @@ -134,6 +143,10 @@ public void AfterFrameApply(ImageFrame source, ImageFrame public CurFrameMetadata DeepClone() => new(this); + /// + /// Copies the observable CUR directory values from an entry. + /// + /// The source directory entry. internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; @@ -142,6 +155,11 @@ internal void FromIconDirEntry(IconDirEntry entry) this.HotspotY = entry.BitCount; } + /// + /// Creates a CUR directory entry from this metadata. + /// + /// The source frame size. + /// The CUR directory entry. internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 @@ -158,6 +176,10 @@ internal IconDirEntry ToIconDirEntry(Size size) }; } + /// + /// Gets the pixel layout represented by this metadata. + /// + /// The represented pixel layout. private PixelTypeInfo GetPixelTypeInfo() { int bpp = (int)this.BmpBitsPerPixel; @@ -219,6 +241,13 @@ private PixelTypeInfo GetPixelTypeInfo() }; } + /// + /// Scales an encoded dimension after an image transform. + /// + /// The encoded source dimension. + /// The full destination dimension. + /// The destination-to-source scale ratio. + /// The encoded destination dimension. private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) @@ -226,9 +255,16 @@ private static byte ScaleEncodingDimension(byte? value, int destination, float r return ClampEncodingDimension(destination); } - return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); + // A stored zero represents 256 pixels, so scaling must expand it before applying the transform ratio. + int source = value.Value is 0 ? 256 : value.Value; + return ClampEncodingDimension(MathF.Ceiling(source * ratio)); } + /// + /// Converts a pixel dimension to the one-byte CUR representation. + /// + /// The pixel dimension. + /// The encoded dimension. private static byte ClampEncodingDimension(float? dimension) => dimension switch { diff --git a/src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs b/src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs new file mode 100644 index 0000000000..3fd951902b --- /dev/null +++ b/src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Cur; + +/// +/// Detects CUR file headers. +/// +public sealed class CurImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => IconDir.Size + IconDirEntry.Size; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? CurFormat.Instance : null; + return format is not null; + } + + /// + /// Determines whether the supplied header contains a valid CUR directory and first entry. + /// + /// The candidate file header. + /// when the header identifies CUR data. + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length < this.HeaderSize) + { + return false; + } + + IconDir dir = IconDir.Parse(header); + if (dir is not { Reserved: 0, Type: IconFileType.CUR } || dir.Count is 0) + { + return false; + } + + IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); + + // The first payload must begin after the complete directory, even when the caller supplied only the detection prefix. + return entry.Reserved is 0 + && entry.BytesInRes is not 0 + && entry.ImageOffset >= IconDir.Size + (dir.Count * IconDirEntry.Size); + } +} diff --git a/src/ImageSharp/Formats/Cur/CurMetadata.cs b/src/ImageSharp/Formats/Cur/CurMetadata.cs index 4c4d83dd88..81090d168f 100644 --- a/src/ImageSharp/Formats/Cur/CurMetadata.cs +++ b/src/ImageSharp/Formats/Cur/CurMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Cur; /// -/// Provides Cur specific metadata information for the image. +/// Provides CUR-specific metadata for an image. /// public class CurMetadata : IFormatMetadata { @@ -32,7 +32,7 @@ private CurMetadata(CurMetadata other) } /// - /// Gets or sets the frame compressions format. Derived from the root frame. + /// Gets or sets the root frame compression format. /// public IconFrameCompression Compression { get; set; } @@ -43,7 +43,7 @@ private CurMetadata(CurMetadata other) public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// - /// Gets or sets the color table, if any. Derived from the root frame.
+ /// Gets or sets the root frame color table, if any.
/// The underlying pixel format is represented by . ///
public ReadOnlyMemory? ColorTable { get; set; } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 06df7b6a25..2b5b740569 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -15,9 +15,10 @@ internal class B44ExrCompression : ExrBaseDecompressor { private readonly int channelCount; - private readonly byte[] scratch = new byte[14]; + // B44 encodes each 4x4 block in either 3 or 14 bytes, so both representations share this inline storage. + private InlineArray14 scratch; - private readonly ushort[] s = new ushort[16]; + private InlineArray16 s; private readonly IMemoryOwner tmpBuffer; @@ -42,8 +43,11 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, { Span outputBuffer = MemoryMarshal.Cast(buffer); Span decompressed = this.tmpBuffer.GetSpan(); + Span scratch = this.scratch; + Span samples = this.s; int outputOffset = 0; int bytesLeft = (int)compressedBytes; + for (int i = 0; i < this.channelCount && bytesLeft > 0; i++) { for (int y = 0; y < this.RowsPerBlock; y += 4) @@ -60,49 +64,49 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, int rowOffset = 0; for (int x = 0; x < this.Width && bytesLeft > 0; x += 4) { - int bytesRead = stream.Read(this.scratch, 0, 3); + int bytesRead = stream.Read(scratch[..3]); if (bytesRead == 0) { ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); } // Check if 3-byte encoded flat field. - if (this.scratch[2] >= 13 << 2) + if (scratch[2] >= 13 << 2) { - Unpack3(this.scratch, this.s); + Unpack3(scratch, samples); bytesLeft -= 3; } else { - bytesRead = stream.Read(this.scratch, 3, 11); + bytesRead = stream.Read(scratch.Slice(3, 11)); if (bytesRead == 0) { ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); } - Unpack14(this.scratch, this.s); + Unpack14(scratch, samples); bytesLeft -= 14; } int n = x + 3 < this.Width ? 4 : this.Width - x; if (y + 3 < this.RowsPerBlock) { - this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); - this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); - this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); - this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]); + samples[..n].CopyTo(row0[rowOffset..]); + samples.Slice(4, n).CopyTo(row1[rowOffset..]); + samples.Slice(8, n).CopyTo(row2[rowOffset..]); + samples.Slice(12, n).CopyTo(row3[rowOffset..]); } else { - this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); + samples[..n].CopyTo(row0[rowOffset..]); if (y + 1 < this.RowsPerBlock) { - this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); + samples.Slice(4, n).CopyTo(row1[rowOffset..]); } if (y + 2 < this.RowsPerBlock) { - this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); + samples.Slice(8, n).CopyTo(row2[rowOffset..]); } } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index abe3ae439e..680e8d333d 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -24,7 +24,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore /// /// Reusable buffer. /// - private readonly byte[] buffer = new byte[8]; + private InlineArray8 buffer; /// /// Used for allocating memory during processing operations. diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index bd74a46b77..5f91d1bd39 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -21,7 +21,7 @@ internal sealed class ExrEncoderCore /// /// Reusable buffer. /// - private readonly byte[] buffer = new byte[8]; + private InlineArray8 buffer; /// /// Used for allocating memory during processing operations. @@ -108,7 +108,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Write magick bytes. BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); // Version number. this.buffer[0] = 2; @@ -117,7 +117,7 @@ public void Encode(Image image, Stream stream, CancellationToken this.buffer[1] = 0; this.buffer[2] = 0; this.buffer[3] = 0; - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); // Write EXR header. this.WriteHeader(stream, header); @@ -194,7 +194,7 @@ private ulong[] EncodeFloatingPointPixelData( // Write row index. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; @@ -237,7 +237,7 @@ private ulong[] EncodeFloatingPointPixelData( // Write pixel row data size. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); stream.Position = pixelDataSizePos; - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); stream.Position = positionAfterPixelData; cancellationToken.ThrowIfCancellationRequested(); @@ -293,7 +293,7 @@ private ulong[] EncodeUnsignedIntPixelData( // Write row index. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; @@ -328,7 +328,7 @@ private ulong[] EncodeUnsignedIntPixelData( // Write pixel row data size. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); stream.Position = pixelDataSizePos; - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); stream.Position = positionAfterPixelData; cancellationToken.ThrowIfCancellationRequested(); @@ -518,7 +518,7 @@ private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) WriteString(stream, channelInfo.ChannelName); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); stream.WriteByte(channelInfo.Linear); @@ -528,10 +528,10 @@ private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) stream.WriteByte(0); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); } /// @@ -629,7 +629,7 @@ private void WriteAttributeInformation(Stream stream, string name, string type, // Write attribute size. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); } /// @@ -656,16 +656,16 @@ private static void WriteString(Stream stream, string str) private void WriteBoxInteger(Stream stream, ExrBox2i box) { BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); } /// @@ -677,7 +677,7 @@ private void WriteBoxInteger(Stream stream, ExrBox2i box) private unsafe void WriteSingle(Stream stream, float value) { BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.Write(this.buffer[..4]); } /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index dc6ce1846b..e4ffd42823 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -22,7 +22,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore /// /// The temp buffer used to reduce allocations. /// - private ScratchBuffer buffer; // mutable struct, don't make readonly + private InlineArray16 buffer; // mutable struct, don't make readonly /// /// The global color table. @@ -285,13 +285,13 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok /// The containing image data. private void ReadGraphicalControlExtension(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer.Span, 0, 6); + int bytesRead = stream.Read(this.buffer, 0, 6); if (bytesRead != 6) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); } - this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer.Span); + this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer); } /// @@ -300,13 +300,13 @@ private void ReadGraphicalControlExtension(BufferedReadStream stream) /// The containing image data. private void ReadImageDescriptor(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer.Span, 0, 9); + int bytesRead = stream.Read(this.buffer, 0, 9); if (bytesRead != 9) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); } - this.imageDescriptor = GifImageDescriptor.Parse(this.buffer.Span); + this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) { GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0"); @@ -321,13 +321,13 @@ private void ReadImageDescriptor(BufferedReadStream stream) /// The containing image data. private void ReadLogicalScreenDescriptor(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer.Span, 0, 7); + int bytesRead = stream.Read(this.buffer, 0, 7); if (bytesRead != 7) { GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); } - this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer.Span); + this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); } /// @@ -353,13 +353,13 @@ private void ReadApplicationExtension(BufferedReadStream stream) // If the length is 11 then it's a valid extension and most likely // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. long position = stream.Position; - int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize); + int bytesRead = stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); if (bytesRead != GifConstants.ApplicationBlockSize) { GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension"); } - bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes); + bool isXmp = ((ReadOnlySpan)this.buffer).StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp) { this.ReadXmpApplicationExtension(stream, position, appLength); @@ -447,13 +447,13 @@ private void ReadNetscapeApplicationExtension(BufferedReadStream stream) => /// The containing image data. private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream) { - int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); + int bytesRead = stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize) { throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension"); } - this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer[1..]).RepeatCount; int terminator = stream.ReadByte(); if (terminator == -1) @@ -996,12 +996,4 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index]; } } - - private unsafe struct ScratchBuffer - { - private const int Size = 16; - private fixed byte scratch[Size]; - - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); - } } diff --git a/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs index 224aaa31ec..adbafc713a 100644 --- a/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs +++ b/src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Icon; - namespace SixLabors.ImageSharp.Formats.Ico; /// -/// Registers the image encoders, decoders and mime type detectors for the Ico format. +/// Registers the image encoder, decoder, and format detector for the ICO format. /// public sealed class IcoConfigurationModule : IImageFormatConfigurationModule { @@ -15,6 +13,6 @@ public void Configure(Configuration configuration) { configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder()); configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance); - configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector()); + configuration.ImageFormatsManager.AddImageFormatDetector(new IcoImageFormatDetector()); } } diff --git a/src/ImageSharp/Formats/Ico/IcoConstants.cs b/src/ImageSharp/Formats/Ico/IcoConstants.cs index 1165793688..1c05cc7178 100644 --- a/src/ImageSharp/Formats/Ico/IcoConstants.cs +++ b/src/ImageSharp/Formats/Ico/IcoConstants.cs @@ -4,12 +4,12 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// -/// Defines constants relating to ICOs +/// Defines constants used by the ICO format. /// internal static class IcoConstants { /// - /// The list of mime types that equate to a ico. + /// The MIME types that identify ICO data. /// /// /// See @@ -27,13 +27,11 @@ internal static class IcoConstants "image/ico", "image/icon", "text/ico", - "application/ico", + "application/ico" ]; /// - /// The list of file extensions that equate to a ico. + /// The file extensions that identify ICO data. /// public static readonly IEnumerable FileExtensions = ["ico"]; - - public const uint FileHeader = 0x00_01_00_00; } diff --git a/src/ImageSharp/Formats/Ico/IcoDecoder.cs b/src/ImageSharp/Formats/Ico/IcoDecoder.cs index a0c8a30752..3be834f6c0 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoder.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoder.cs @@ -6,10 +6,13 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// -/// Decoder for generating an image out of a ico encoded stream. +/// Decoder for generating an image from an ICO encoded stream. /// public sealed class IcoDecoder : ImageDecoder { + /// + /// Initializes a new instance of the class. + /// private IcoDecoder() { } @@ -34,7 +37,7 @@ protected override Image Decode(DecoderOptions options, Stream s /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => this.Decode(options, stream, cancellationToken); + => this.Decode(options, stream, cancellationToken); /// protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) diff --git a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs index b8a1dded15..463e620514 100644 --- a/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoDecoderCore.cs @@ -7,13 +7,21 @@ namespace SixLabors.ImageSharp.Formats.Ico; +/// +/// Decodes ICO containers and maps directory metadata to ICO metadata. +/// internal sealed class IcoDecoderCore : IconDecoderCore { + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. public IcoDecoderCore(DecoderOptions options) - : base(options) + : base(options, IconFileType.ICO) { } + /// protected override void SetFrameMetadata( ImageMetadata imageMetadata, ImageFrameMetadata frameMetadata, @@ -31,10 +39,10 @@ protected override void SetFrameMetadata( if (index == 0) { - IcoMetadata curMetadata = imageMetadata.GetIcoMetadata(); - curMetadata.Compression = compression; - curMetadata.BmpBitsPerPixel = bitsPerPixel; - curMetadata.ColorTable = colorTable; + IcoMetadata icoMetadata = imageMetadata.GetIcoMetadata(); + icoMetadata.Compression = compression; + icoMetadata.BmpBitsPerPixel = bitsPerPixel; + icoMetadata.ColorTable = colorTable; } } } diff --git a/src/ImageSharp/Formats/Ico/IcoEncoder.cs b/src/ImageSharp/Formats/Ico/IcoEncoder.cs index e729251567..36c661419d 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoder.cs +++ b/src/ImageSharp/Formats/Ico/IcoEncoder.cs @@ -10,8 +10,5 @@ public sealed class IcoEncoder : QuantizingImageEncoder { /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) - { - IcoEncoderCore encoderCore = new(this); - encoderCore.Encode(image, stream, cancellationToken); - } + => new IcoEncoderCore(this).Encode(image, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs index f3cacb1b96..0364ce6ab8 100644 --- a/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs +++ b/src/ImageSharp/Formats/Ico/IcoEncoderCore.cs @@ -2,13 +2,46 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ico; +/// +/// Encodes ICO containers using ICO frame metadata. +/// internal sealed class IcoEncoderCore : IconEncoderCore { + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. public IcoEncoderCore(QuantizingImageEncoder encoder) : base(encoder, IconFileType.ICO) { } + + /// + /// Encodes all source frames as an ICO resource. + /// + /// The source pixel type. + /// The source image. + /// The destination stream. + /// The token to monitor for cancellation requests. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + => this.Encode(image, stream, default(IcoFrameMetadataProvider), cancellationToken); + + /// + /// Supplies ICO frame metadata to the shared container encoder. + /// + private readonly struct IcoFrameMetadataProvider : IEncodingFrameMetadataProvider + { + /// + public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory? colorTable) + { + IcoFrameMetadata metadata = frame.Metadata.GetIcoMetadata(); + colorTable = metadata.ColorTable; + return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry(frame.Size)); + } + } } diff --git a/src/ImageSharp/Formats/Ico/IcoFormat.cs b/src/ImageSharp/Formats/Ico/IcoFormat.cs index 5f89ab7f28..43621a496e 100644 --- a/src/ImageSharp/Formats/Ico/IcoFormat.cs +++ b/src/ImageSharp/Formats/Ico/IcoFormat.cs @@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// public sealed class IcoFormat : IImageFormat { + /// + /// Prevents a default instance of the class from being created. + /// private IcoFormat() { } @@ -21,7 +24,7 @@ private IcoFormat() public string Name => "ICO"; /// - public string DefaultMimeType => IcoConstants.MimeTypes.First(); + public string DefaultMimeType => "image/vnd.microsoft.icon"; /// public IEnumerable MimeTypes => IcoConstants.MimeTypes; diff --git a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs index fb42d60437..829fc6b999 100644 --- a/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// -/// Provides Ico specific metadata information for the image frame. +/// Provides ICO-specific metadata for an image frame. /// public class IcoFrameMetadata : IFormatFrameMetadata { @@ -20,6 +20,10 @@ public IcoFrameMetadata() { } + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The metadata to copy. private IcoFrameMetadata(IcoFrameMetadata other) { this.Compression = other.Compression; @@ -34,19 +38,19 @@ private IcoFrameMetadata(IcoFrameMetadata other) } /// - /// Gets or sets the frame compressions format. + /// Gets or sets the frame compression format. /// public IconFrameCompression Compression { get; set; } /// - /// Gets or sets the encoding width.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded width. + /// A value of zero represents 256 pixels or greater. ///
public byte? EncodingWidth { get; set; } /// - /// Gets or sets the encoding height.
- /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + /// Gets or sets the encoded height. + /// A value of zero represents 256 pixels or greater. ///
public byte? EncodingHeight { get; set; } @@ -127,12 +131,21 @@ public void AfterFrameApply(ImageFrame source, ImageFrame public IcoFrameMetadata DeepClone() => new(this); + /// + /// Copies the observable ICO directory values from an entry. + /// + /// The source directory entry. internal void FromIconDirEntry(IconDirEntry entry) { this.EncodingWidth = entry.Width; this.EncodingHeight = entry.Height; } + /// + /// Creates an ICO directory entry from this metadata. + /// + /// The source frame size. + /// The ICO directory entry. internal IconDirEntry ToIconDirEntry(Size size) { byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 @@ -148,11 +161,15 @@ internal IconDirEntry ToIconDirEntry(Size size) BitCount = this.Compression switch { IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel, - IconFrameCompression.Png or _ => 32, - }, + IconFrameCompression.Png or _ => 32 + } }; } + /// + /// Gets the pixel layout represented by this metadata. + /// + /// The represented pixel layout. private PixelTypeInfo GetPixelTypeInfo() { int bpp = (int)this.BmpBitsPerPixel; @@ -214,6 +231,13 @@ private PixelTypeInfo GetPixelTypeInfo() }; } + /// + /// Scales an encoded dimension after an image transform. + /// + /// The encoded source dimension. + /// The full destination dimension. + /// The destination-to-source scale ratio. + /// The encoded destination dimension. private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) { if (value is null) @@ -221,9 +245,16 @@ private static byte ScaleEncodingDimension(byte? value, int destination, float r return ClampEncodingDimension(destination); } - return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio)); + // A stored zero represents 256 pixels, so scaling must expand it before applying the transform ratio. + int source = value.Value is 0 ? 256 : value.Value; + return ClampEncodingDimension(MathF.Ceiling(source * ratio)); } + /// + /// Converts a pixel dimension to the one-byte ICO representation. + /// + /// The pixel dimension. + /// The encoded dimension. private static byte ClampEncodingDimension(float? dimension) => dimension switch { diff --git a/src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs b/src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs new file mode 100644 index 0000000000..78e9634dd0 --- /dev/null +++ b/src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Icon; + +namespace SixLabors.ImageSharp.Formats.Ico; + +/// +/// Detects ICO file headers. +/// +public sealed class IcoImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => IconDir.Size + IconDirEntry.Size; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? IcoFormat.Instance : null; + return format is not null; + } + + /// + /// Determines whether the supplied header contains a valid ICO directory and first entry. + /// + /// The candidate file header. + /// when the header identifies ICO data. + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length < this.HeaderSize) + { + return false; + } + + IconDir dir = IconDir.Parse(header); + if (dir is not { Reserved: 0, Type: IconFileType.ICO } || dir.Count is 0) + { + return false; + } + + IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); + + // The first payload must begin after the complete directory, even when the caller supplied only the detection prefix. + return entry is { Reserved: 0, Planes: 0 or 1, BitCount: 1 or 4 or 8 or 16 or 24 or 32 } + && entry.BytesInRes is not 0 + && entry.ImageOffset >= IconDir.Size + (dir.Count * IconDirEntry.Size); + } +} diff --git a/src/ImageSharp/Formats/Ico/IcoMetadata.cs b/src/ImageSharp/Formats/Ico/IcoMetadata.cs index ae768d3111..943b6338cc 100644 --- a/src/ImageSharp/Formats/Ico/IcoMetadata.cs +++ b/src/ImageSharp/Formats/Ico/IcoMetadata.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Formats.Ico; /// -/// Provides Ico specific metadata information for the image. +/// Provides ICO-specific metadata for an image. /// public class IcoMetadata : IFormatMetadata { @@ -32,7 +32,7 @@ private IcoMetadata(IcoMetadata other) } /// - /// Gets or sets the frame compressions format. Derived from the root frame. + /// Gets or sets the root frame compression format. /// public IconFrameCompression Compression { get; set; } @@ -43,7 +43,7 @@ private IcoMetadata(IcoMetadata other) public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; /// - /// Gets or sets the color table, if any. Derived from the root frame.
+ /// Gets or sets the root frame color table, if any.
/// The underlying pixel format is represented by . ///
public ReadOnlyMemory? ColorTable { get; set; } diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index bc3258e6b2..c1c6ee3080 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -9,15 +9,28 @@ namespace SixLabors.ImageSharp.Formats.Icon; +/// +/// Decodes the shared ICO/CUR directory and embedded BMP or PNG frame payloads. +/// internal abstract class IconDecoderCore : ImageDecoderCore { + private readonly IconFileType iconFileType; private IconDir fileHeader; private IconDirEntry[]? entries; - protected IconDecoderCore(DecoderOptions options) + /// + /// Reusable storage for an icon directory entry and smaller fixed values. + /// + private InlineArray16 buffer; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + /// The expected icon container type. + protected IconDecoderCore(DecoderOptions options, IconFileType iconFileType) : base(options) - { - } + => this.iconFileType = iconFileType; /// protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -26,108 +39,126 @@ protected override Image Decode(BufferedReadStream stream, Cance long basePosition = stream.Position; this.ReadHeader(stream); - Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; - - List<(Image Image, IconFrameCompression Compression, int Index)> decodedEntries - = new((int)Math.Min(this.entries.Length, this.Options.MaxFrames)); + int entryCount = this.entries.Length; + (int EntryIndex, Image Image, IconFrameCompression Compression)[] decodedEntries = new (int, Image, IconFrameCompression)[entryCount]; + int decodedCount = 0; + IconFrameStream frameStream = new(stream); + this.Dimensions = default; - for (int i = 0; i < this.entries.Length; i++) + try { - if (i == this.Options.MaxFrames) + for (int i = 0; i < entryCount; i++) { - break; - } - - ref IconDirEntry entry = ref this.entries[i]; + int entryIndex = 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; - } + this.ExecuteImageDataSegmentAction(() => + { + cancellationToken.ThrowIfCancellationRequested(); - // Reset the stream position. - _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); + ref IconDirEntry entry = ref this.entries[entryIndex]; + this.SetFrameStreamBounds(frameStream, stream, basePosition, entry); + Span flag = this.buffer[..PngConstants.HeaderBytes.Length]; + CheckEndOfStream(frameStream.Read(flag), flag.Length); + frameStream.Position = 0; - bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + IconFrameCompression compression = isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp; - // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. - Image temp = this.GetDecoder(isPng).Decode(this.Options.Configuration, stream, cancellationToken); - decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i)); + // Frames remain alive until the largest decoded dimensions are known and the common canvas can be allocated. + Image decoded = this.GetDecoder(isPng).Decode(this.Options.Configuration, frameStream, cancellationToken); + decodedEntries[decodedCount++] = (entryIndex, decoded, compression); - // 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 Size(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height)); - } + // The embedded header is authoritative because a zero directory dimension can represent 256 pixels or a larger Vista-era PNG. + this.Dimensions = new(Math.Max(this.Dimensions.Width, decoded.Width), Math.Max(this.Dimensions.Height, decoded.Height)); + }); + } - ImageMetadata metadata = new(); - BmpMetadata? bmpMetadata = null; - PngMetadata? pngMetadata = null; - Image result = new(this.Options.Configuration, metadata, decodedEntries.Select(x => - { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; - ReadOnlyMemory? colorTable = null; - ImageFrame target = new(this.Options.Configuration, this.Dimensions); - ImageFrame source = x.Image.Frames.RootFrameUnsafe; - for (int y = 0; y < source.Height; y++) + if (decodedCount is 0) { - source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + throw new InvalidImageContentException("The icon file does not contain any decodable image entries."); } - // Copy the format specific frame metadata to the image. - if (x.Compression is IconFrameCompression.Png) + ImageMetadata metadata = new(); + BmpMetadata? bmpMetadata = null; + PngMetadata? pngMetadata = null; + ImageFrame[] frames = new ImageFrame[decodedCount]; + int initializedFrameCount = 0; + + try { - if (x.Index == 0) + for (int i = 0; i < decodedCount; i++) { - pngMetadata = x.Image.Metadata.GetPngMetadata(); + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; + ReadOnlyMemory? colorTable = null; + Image decoded = decodedEntries[i].Image; + ref IconDirEntry entry = ref this.entries[decodedEntries[i].EntryIndex]; + ImageFrame source = decoded.Frames.RootFrameUnsafe; + ImageFrame target = new(this.Options.Configuration, this.Dimensions); + frames[i] = target; + initializedFrameCount++; + + for (int y = 0; y < source.Height; y++) + { + source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + } + + // Preserve both the embedded format metadata and the ICO/CUR directory metadata on the output frame. + if (decodedEntries[i].Compression is IconFrameCompression.Png) + { + if (i == 0) + { + pngMetadata = decoded.Metadata.GetPngMetadata(); + } + + target.Metadata.SetFormatMetadata(PngFormat.Instance, source.Metadata.GetPngMetadata()); + } + else + { + BmpMetadata currentBmpMetadata = decoded.Metadata.GetBmpMetadata(); + bitsPerPixel = currentBmpMetadata.BitsPerPixel; + colorTable = currentBmpMetadata.ColorTable; + + if (i == 0) + { + bmpMetadata = currentBmpMetadata; + } + } + + this.SetFrameMetadata(metadata, target.Metadata, i, entry, decodedEntries[i].Compression, bitsPerPixel, colorTable); } - 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) + // Embedded metadata belongs to the container even though the temporary decoded images are disposed below. + if (bmpMetadata is not null) { - bmpMetadata = meta; + metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); } - } - - this.SetFrameMetadata( - metadata, - target.Metadata, - x.Index, - this.entries[x.Index], - x.Compression, - bitsPerPixel, - colorTable); - x.Image.Dispose(); + if (pngMetadata is not null) + { + metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); + } - return target; - }).ToArray()); + Image result = new(this.Options.Configuration, metadata, frames); - // Copy the format specific metadata to the image. - if (bmpMetadata != null) - { - result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); + // Ownership of every output frame transfers to the result only after construction succeeds. + initializedFrameCount = 0; + return result; + } + finally + { + for (int i = 0; i < initializedFrameCount; i++) + { + frames[i].Dispose(); + } + } } - - if (pngMetadata != null) + finally { - result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); + for (int i = 0; i < decodedCount; i++) + { + decodedEntries[i].Image.Dispose(); + } } - - return result; } /// @@ -137,87 +168,82 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok long basePosition = stream.Position; this.ReadHeader(stream); - Span flag = stackalloc byte[PngConstants.HeaderBytes.Length]; - ImageMetadata metadata = new(); BmpMetadata? bmpMetadata = null; PngMetadata? pngMetadata = null; - ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)]; - int bpp = 0; + ImageFrameMetadata[] frames = new ImageFrameMetadata[this.entries.Length]; + int frameCount = 0; + IconFrameStream frameStream = new(stream); + this.Dimensions = default; + for (int i = 0; i < frames.Length; i++) { - BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; - ReadOnlyMemory? colorTable = null; - ref IconDirEntry entry = ref this.entries[i]; + int entryIndex = i; - // If we hit the end of the stream we should break. - if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length) + this.ExecuteImageDataSegmentAction(() => { - break; - } - - // There should always be enough bytes for this regardless of the entry type. - if (stream.Read(flag) != PngConstants.HeaderBytes.Length) - { - break; - } + cancellationToken.ThrowIfCancellationRequested(); - // Reset the stream position. - _ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current); + BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32; + ReadOnlyMemory? colorTable = null; + ref IconDirEntry entry = ref this.entries[entryIndex]; + this.SetFrameStreamBounds(frameStream, stream, basePosition, entry); + Span flag = this.buffer[..PngConstants.HeaderBytes.Length]; + CheckEndOfStream(frameStream.Read(flag), flag.Length); + frameStream.Position = 0; - bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); + ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, frameStream, cancellationToken); + ImageFrameMetadata frameMetadata = new(); - // Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result. - ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken); - - ImageFrameMetadata frameMetadata = new(); - - if (isPng) - { - if (i == 0) + if (isPng) { - pngMetadata = frameInfo.Metadata.GetPngMetadata(); - } + if (frameCount is 0) + { + pngMetadata = frameInfo.Metadata.GetPngMetadata(); + } - frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata()); - } - else - { - BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata(); - bitsPerPixel = meta.BitsPerPixel; - colorTable = meta.ColorTable; - - if (i == 0) + frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata()); + } + else { - bmpMetadata = meta; + BmpMetadata currentBmpMetadata = frameInfo.Metadata.GetBmpMetadata(); + bitsPerPixel = currentBmpMetadata.BitsPerPixel; + colorTable = currentBmpMetadata.ColorTable; + + if (frameCount is 0) + { + bmpMetadata = currentBmpMetadata; + } } - } - bpp = Math.Max(bpp, (int)bitsPerPixel); + IconFrameCompression compression = isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp; + this.SetFrameMetadata(metadata, frameMetadata, frameCount, entry, compression, bitsPerPixel, colorTable); + frames[frameCount++] = frameMetadata; - frames[i] = frameMetadata; + // Identification uses the same embedded-header dimensions as decoding, without allocating pixel buffers. + this.Dimensions = new(Math.Max(this.Dimensions.Width, frameInfo.Width), Math.Max(this.Dimensions.Height, frameInfo.Height)); + }); + } - this.SetFrameMetadata( - metadata, - frames[i], - i, - this.entries[i], - isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, - bitsPerPixel, - colorTable); + if (frameCount is 0) + { + throw new InvalidImageContentException("The icon file does not contain any identifiable image entries."); + } - // 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 Size(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height)); + if (frameCount != frames.Length) + { + // Preserve successfully identified frames when truncated image data ends the scan before the declared directory count. + Array.Resize(ref frames, frameCount); } // Copy the format specific metadata to the image. - if (bmpMetadata != null) + if (bmpMetadata is not null) { metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata); } - if (pngMetadata != null) + if (pngMetadata is not null) { metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata); } @@ -225,6 +251,16 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok return new ImageInfo(this.Dimensions, metadata, frames); } + /// + /// Copies format-specific directory and embedded-frame metadata to an output frame. + /// + /// The output image metadata. + /// The output frame metadata. + /// The directory entry index. + /// The directory entry. + /// The embedded frame compression. + /// The embedded bitmap bit depth. + /// The embedded bitmap color table. protected abstract void SetFrameMetadata( ImageMetadata imageMetadata, ImageFrameMetadata frameMetadata, @@ -234,58 +270,45 @@ protected abstract void SetFrameMetadata( BmpBitsPerPixel bitsPerPixel, ReadOnlyMemory? colorTable); + /// + /// Reads the icon directory entries needed by the configured frame limit. + /// + /// The source stream. [MemberNotNull(nameof(entries))] - protected void ReadHeader(Stream stream) + private void ReadHeader(Stream stream) { - Span buffer = stackalloc byte[IconDirEntry.Size]; + Span buffer = this.buffer; // ICONDIR - _ = CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size); + CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size); this.fileHeader = IconDir.Parse(buffer); + if (this.fileHeader.Reserved != 0 || this.fileHeader.Type != this.iconFileType || this.fileHeader.Count == 0) + { + throw new InvalidImageContentException("The icon directory header is invalid."); + } // ICONDIRENTRY - this.entries = new IconDirEntry[this.fileHeader.Count]; + int entryCount = (int)Math.Min(this.fileHeader.Count, this.Options.MaxFrames); + this.entries = new IconDirEntry[entryCount]; for (int i = 0; i < this.entries.Length; i++) { - _ = CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size); + 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 Size(width, height); } + /// + /// Creates the decoder configured for an embedded PNG or headerless, double-height bitmap frame. + /// + /// Whether the embedded frame has a PNG signature. + /// The configured frame decoder. private ImageDecoderCore GetDecoder(bool isPng) { if (isPng) { return new PngDecoderCore(new PngDecoderOptions { - GeneralOptions = this.Options, + GeneralOptions = this.Options }); } @@ -294,17 +317,59 @@ private ImageDecoderCore GetDecoder(bool isPng) GeneralOptions = this.Options, ProcessedAlphaMask = true, SkipFileHeader = true, - UseDoubleHeight = true, + UseDoubleHeight = true }); } - private static int CheckEndOfStream(int v, int length) + /// + /// Creates a seekable view bounded to one directory entry's declared payload. + /// + /// The reusable bounded payload stream. + /// The containing icon stream. + /// The absolute start of the icon resource. + /// The directory entry describing the payload. + private void SetFrameStreamBounds(IconFrameStream frameStream, BufferedReadStream stream, long basePosition, in IconDirEntry entry) { - if (v != length) + long available = stream.Length - basePosition; + uint directorySize = (uint)(IconDir.Size + (this.fileHeader.Count * IconDirEntry.Size)); + + // Offsets are relative to the icon resource and must not point into its directory or beyond its containing stream. + if (entry.Reserved is not 0 + || entry.BytesInRes is 0 + || entry.ImageOffset < directorySize + || entry.ImageOffset > available) { - throw new InvalidImageContentException("Not enough bytes to read icon header."); + throw new InvalidImageContentException("The icon directory contains an invalid image resource range."); } - return v; + long remaining = available - entry.ImageOffset; + long length = entry.BytesInRes; + if (length > remaining) + { + if (this.fileHeader.Count is not 1) + { + // Clamping a multi-entry resource could expose the next image payload to the current child decoder. + throw new InvalidImageContentException("The icon directory contains an invalid image resource range."); + } + + // Some established single-image ICO files overstate BytesInRes but contain a complete payload. + // The containing stream is still a safe hard boundary because no sibling image can follow it. + length = remaining; + } + + frameStream.Reset(basePosition + entry.ImageOffset, length); + } + + /// + /// Ensures that a complete fixed-size directory structure was read. + /// + /// The number of bytes read. + /// The required structure length. + private static void CheckEndOfStream(int bytesRead, int expectedLength) + { + if (bytesRead != expectedLength) + { + throw new InvalidImageContentException("Not enough bytes to read icon header."); + } } } diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index 36d5d8820e..06644fc6c2 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/src/ImageSharp/Formats/Icon/IconDir.cs @@ -1,14 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; +/// +/// Represents an ICO or CUR file directory header. +/// [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] internal struct IconDir { + /// + /// The serialized directory-header size in bytes. + /// public const int Size = 3 * sizeof(ushort); /// @@ -26,26 +31,39 @@ internal struct IconDir /// public ushort Count; + /// + /// Initializes a new instance of the struct. + /// + /// The icon file type. public IconDir(IconFileType type) : this(type, 0) { } + /// + /// Initializes a new instance of the struct. + /// + /// The icon file type. + /// The number of directory entries. public IconDir(IconFileType type, ushort count) - : this(0, type, count) { - } - - public IconDir(ushort reserved, IconFileType type, ushort count) - { - this.Reserved = reserved; + this.Reserved = 0; this.Type = type; this.Count = count; } - public static ref IconDir Parse(ReadOnlySpan data) - => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + /// + /// Parses an icon directory header from its byte representation. + /// + /// The icon directory header data. + /// The parsed icon directory header. + public static IconDir Parse(ReadOnlySpan data) + => MemoryMarshal.Cast(data)[0]; + /// + /// Writes the icon directory header to the destination stream. + /// + /// The destination stream. public readonly void WriteTo(Stream stream) - => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); + => stream.Write(MemoryMarshal.Cast([this])); } diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs index 598ec47b6e..9f74c5379e 100644 --- a/src/ImageSharp/Formats/Icon/IconDirEntry.cs +++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; @@ -9,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Icon; [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] internal struct IconDirEntry { + /// + /// The serialized directory-entry size in bytes. + /// public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint)); /// @@ -17,7 +19,7 @@ internal struct IconDirEntry public byte Width; /// - /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[ + /// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels. /// public byte Height; @@ -44,7 +46,7 @@ internal struct IconDirEntry public ushort BitCount; /// - /// Specifies the size of the image's data in bytes + /// Specifies the size of the image's data in bytes. /// public uint BytesInRes; @@ -53,9 +55,18 @@ internal struct IconDirEntry /// public uint ImageOffset; - public static ref IconDirEntry Parse(in ReadOnlySpan data) - => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); + /// + /// Parses an icon directory entry from its byte representation. + /// + /// The icon directory entry data. + /// The parsed icon directory entry. + public static IconDirEntry Parse(ReadOnlySpan data) + => MemoryMarshal.Cast(data)[0]; - public readonly void WriteTo(in Stream stream) - => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); + /// + /// Writes the icon directory entry to the destination stream. + /// + /// The destination stream. + public readonly void WriteTo(Stream stream) + => stream.Write(MemoryMarshal.Cast([this])); } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 479cf9f1b0..97bf96ea2a 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -1,111 +1,187 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics.CodeAnalysis; +using System.Buffers; using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Cur; -using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Icon; +/// +/// Encodes ICO and CUR containers. +/// internal abstract class IconEncoderCore { private readonly QuantizingImageEncoder encoder; private readonly IconFileType iconFileType; - private IconDir fileHeader; - private EncodingFrameMetadata[]? entries; + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The icon container type. protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType) { this.encoder = encoder; this.iconFileType = iconFileType; } - public void Encode( - Image image, - Stream stream, - CancellationToken cancellationToken) + /// + /// Supplies icon directory and color-table metadata without allocating intermediary metadata objects. + /// + internal interface IEncodingFrameMetadataProvider + { + /// + /// Gets the encoding metadata for a source frame. + /// + /// The source frame. + /// The optional bitmap color table. + /// The encoding metadata. + public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory? colorTable); + } + + /// + /// Encodes all source frames using the metadata provider owned by the concrete icon format. + /// + /// The source pixel type. + /// The metadata provider type. + /// The source image. + /// The destination stream. + /// The frame metadata provider. + /// The token to monitor for cancellation requests. + protected void Encode(Image image, Stream stream, TProvider provider, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel + where TProvider : struct, IEncodingFrameMetadataProvider { Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - // Stream may not at 0. + // Directory metadata is unmanaged and short-lived, so allocator-owned storage avoids an array plus one object per frame. + using IMemoryOwner owner = image.Configuration.MemoryAllocator.Allocate(image.Frames.Count); + Span entries = owner.GetSpan()[..image.Frames.Count]; + this.Encode(image, stream, 0, entries, provider, cancellationToken); + } + + /// + /// Encodes a contiguous source-frame range using a stack-only metadata provider. + /// + /// The source pixel type. + /// The metadata provider type. + /// The source image. + /// The destination stream. + /// The first source-frame index. + /// The directory metadata for the source frames. + /// The frame metadata provider. + /// The token to monitor for cancellation requests. + internal void Encode(Image image, Stream stream, int frameIndex, Span entries, TProvider provider, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + where TProvider : struct, IEncodingFrameMetadataProvider + { + if ((uint)entries.Length > ushort.MaxValue) + { + throw new ImageFormatException("ICO and CUR resources cannot contain more than 65535 directory entries."); + } + + // Offsets stored in ICO/CUR entries are relative to the start of this child resource, not the containing ANI stream. long basePosition = stream.Position; - this.InitHeader(image); + IconDir fileHeader = new(this.iconFileType, (ushort)entries.Length); - // 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); + // Reserve the directory first because BytesInRes and ImageOffset are known only after each payload is encoded. + int dataOffset = IconDir.Size + (IconDirEntry.Size * entries.Length); _ = stream.Seek(dataOffset, SeekOrigin.Current); - for (int i = 0; i < image.Frames.Count; i++) + for (int i = 0; i < entries.Length; i++) { cancellationToken.ThrowIfCancellationRequested(); // Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data // which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft. - ImageFrame frame = image.Frames[i]; - int width = this.entries[i].Entry.Width; + ImageFrame frame = image.Frames[frameIndex + i]; + + // The struct provider is statically dispatched, avoiding boxing and intermediary ICO/CUR metadata allocations. + // Only unmanaged directory data survives until backpatching; the managed color table is consumed for this frame. + entries[i] = provider.GetEncodingFrameMetadata(frame, out ReadOnlyMemory? colorTable); + int width = entries[i].Entry.Width; if (width is 0) { width = frame.Width; } - int height = this.entries[i].Entry.Height; + int height = entries[i].Entry.Height; if (height is 0) { height = frame.Height; } - this.entries[i].Entry.ImageOffset = (uint)stream.Position; + long imageStart = stream.Position; + entries[i].Entry.ImageOffset = checked((uint)(imageStart - basePosition)); - // We crop the frame to the size specified in the metadata. - using Image encodingFrame = new(width, height); + // ANI flattens variants onto a common canvas, while an icon entry can encode a smaller rectangle. + // Child encoders consume Image, so an isolated cropped image is required to prevent encoding the padded canvas. + using Image encodingFrame = new(image.Configuration, width, height); for (int y = 0; y < height; y++) { - frame.PixelBuffer.DangerousGetRowSpan(y)[..width] - .CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); + frame.PixelBuffer.DangerousGetRowSpan(y)[..width].CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); } - ref EncodingFrameMetadata encodingMetadata = ref this.entries[i]; + ref EncodingFrameMetadata encodingMetadata = ref entries[i]; - QuantizingImageEncoder encoder = encodingMetadata.Compression switch + // Compression and bitmap depth are per-entry, so the concrete encoder configuration must be selected per frame. + switch (encodingMetadata.Compression) { - IconFrameCompression.Bmp => new BmpEncoder() + case IconFrameCompression.Bmp: { - Quantizer = this.GetQuantizer(encodingMetadata), - ProcessedAlphaMask = true, - UseDoubleHeight = true, - SkipFileHeader = true, - SupportTransparency = false, - TransparentColorMode = this.encoder.TransparentColorMode, - PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, - BitsPerPixel = encodingMetadata.BmpBitsPerPixel - }, - IconFrameCompression.Png => new PngEncoder() + BmpEncoder bmpEncoder = new() + { + Quantizer = this.GetQuantizer(encodingMetadata, colorTable), + ProcessedAlphaMask = true, + UseDoubleHeight = true, + SkipFileHeader = true, + SupportTransparency = false, + TransparentColorMode = this.encoder.TransparentColorMode, + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + BitsPerPixel = encodingMetadata.BmpBitsPerPixel, + SkipMetadata = this.encoder.SkipMetadata + }; + + BmpEncoderCore bmpEncoderCore = new(bmpEncoder, image.Configuration.MemoryAllocator); + bmpEncoderCore.Encode(encodingFrame, stream, cancellationToken); + break; + } + + case IconFrameCompression.Png: { - // Only 32bit Png supported. - // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 - BitDepth = PngBitDepth.Bit8, - ColorType = PngColorType.RgbWithAlpha, - TransparentColorMode = this.encoder.TransparentColorMode, - CompressionLevel = PngCompressionLevel.BestCompression - }, - _ => throw new NotSupportedException(), - }; - - encoder.Encode(encodingFrame, stream); - encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset; + PngEncoder pngEncoder = new() + { + // Only 32bit Png supported. + // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 + BitDepth = PngBitDepth.Bit8, + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = this.encoder.TransparentColorMode, + CompressionLevel = PngCompressionLevel.BestCompression, + SkipMetadata = this.encoder.SkipMetadata + }; + + using PngEncoderCore pngEncoderCore = new(image.Configuration, pngEncoder); + pngEncoderCore.Encode(encodingFrame, stream, cancellationToken); + break; + } + + default: + throw new NotSupportedException(); + } + + encodingMetadata.Entry.BytesInRes = checked((uint)(stream.Position - imageStart)); } - // We now need to rewind the stream and write the header and the entries. + // Backpatch the reserved directory after every relative offset and payload length has been measured. long endPosition = stream.Position; _ = stream.Seek(basePosition, SeekOrigin.Begin); - this.fileHeader.WriteTo(stream); - foreach (EncodingFrameMetadata frame in this.entries) + fileHeader.WriteTo(stream); + foreach (EncodingFrameMetadata frame in entries) { frame.Entry.WriteTo(stream); } @@ -113,31 +189,16 @@ public void Encode( _ = stream.Seek(endPosition, SeekOrigin.Begin); } - [MemberNotNull(nameof(entries))] - private void InitHeader(Image image) - { - this.fileHeader = new IconDir(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(i.Size)); - })], - IconFileType.CUR => - [.. image.Frames.Select(i => - { - CurFrameMetadata metadata = i.Metadata.GetCurMetadata(); - return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size)); - })], - _ => throw new NotSupportedException(), - }; - } - - private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata) + /// + /// Gets the quantizer for an embedded bitmap frame. + /// + /// The frame encoding metadata. + /// The optional bitmap color table. + /// The configured quantizer, or when quantization is not required. + private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata, ReadOnlyMemory? colorTable) { - if (metadata.Entry.BitCount > 8) + // CUR stores its vertical hotspot in Entry.BitCount, so quantization must use the independent bitmap depth. + if (metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8) { return null; } @@ -147,7 +208,7 @@ [.. image.Frames.Select(i => return this.encoder.Quantizer; } - if (metadata.ColorTable is null) + if (colorTable is null) { int count = metadata.Entry.ColorCount; if (count == 0) @@ -162,33 +223,42 @@ [.. image.Frames.Select(i => } // Don't dither if we have a palette. We want to preserve as much information as possible. - return new PaletteQuantizer(metadata.ColorTable.Value, new QuantizerOptions { Dither = null }); + return new PaletteQuantizer(colorTable.Value, new QuantizerOptions { Dither = null }); } - internal sealed class EncodingFrameMetadata + /// + /// Stores the unmanaged per-frame state required while an icon directory is backpatched. + /// + internal struct EncodingFrameMetadata { - private IconDirEntry iconDirEntry; + /// + /// The icon directory entry. + /// + public IconDirEntry Entry; - public EncodingFrameMetadata( - IconFrameCompression compression, - BmpBitsPerPixel bmpBitsPerPixel, - ReadOnlyMemory? colorTable, - IconDirEntry iconDirEntry) + /// + /// Initializes a new instance of the struct. + /// + /// The embedded image compression. + /// The bitmap bit depth. + /// The icon directory entry. + public EncodingFrameMetadata(IconFrameCompression compression, BmpBitsPerPixel bmpBitsPerPixel, IconDirEntry iconDirEntry) { this.Compression = compression; this.BmpBitsPerPixel = compression == IconFrameCompression.Png ? BmpBitsPerPixel.Bit32 : bmpBitsPerPixel; - this.ColorTable = colorTable; - this.iconDirEntry = iconDirEntry; + this.Entry = iconDirEntry; } + /// + /// Gets the embedded image compression. + /// public IconFrameCompression Compression { get; } + /// + /// Gets the bitmap bit depth. + /// public BmpBitsPerPixel BmpBitsPerPixel { get; } - - public ReadOnlyMemory? ColorTable { get; set; } - - public ref IconDirEntry Entry => ref this.iconDirEntry; } } diff --git a/src/ImageSharp/Formats/Icon/IconFileType.cs b/src/ImageSharp/Formats/Icon/IconFileType.cs index 3c13227d7d..0b94dc71f9 100644 --- a/src/ImageSharp/Formats/Icon/IconFileType.cs +++ b/src/ImageSharp/Formats/Icon/IconFileType.cs @@ -4,17 +4,17 @@ namespace SixLabors.ImageSharp.Formats.Icon; /// -/// Ico file type +/// Identifies the type stored in an ICO or CUR directory header. /// internal enum IconFileType : ushort { /// - /// ICO file + /// A Windows icon file. /// ICO = 1, /// - /// CUR file + /// A Windows cursor file. /// CUR = 2 } diff --git a/src/ImageSharp/Formats/Icon/IconFrameCompression.cs b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs index 5c772c3fe3..f0dbe62f29 100644 --- a/src/ImageSharp/Formats/Icon/IconFrameCompression.cs +++ b/src/ImageSharp/Formats/Icon/IconFrameCompression.cs @@ -4,17 +4,17 @@ namespace SixLabors.ImageSharp.Formats.Icon; /// -/// IconFrameCompression +/// Specifies the encoding used for an image embedded in an ICO or CUR resource. /// public enum IconFrameCompression { /// - /// Bmp + /// The image is encoded as a headerless Windows bitmap with an AND transparency mask. /// Bmp, /// - /// Png + /// The image is encoded as PNG data. /// Png } diff --git a/src/ImageSharp/Formats/Icon/IconFrameStream.cs b/src/ImageSharp/Formats/Icon/IconFrameStream.cs new file mode 100644 index 0000000000..06b00b4b05 --- /dev/null +++ b/src/ImageSharp/Formats/Icon/IconFrameStream.cs @@ -0,0 +1,114 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Icon; + +/// +/// Exposes one ICO or CUR directory entry as an isolated seekable stream. +/// +/// +/// Embedded decoders accept seek offsets from their own headers. Bounding those seeks to BytesInRes +/// prevents a malformed BMP or PNG payload from consuming an adjacent icon resource. +/// +internal sealed class IconFrameStream : Stream +{ + private readonly Stream stream; + + // start is absolute in the containing stream; position is always relative to this bounded resource. + private long start; + private long length; + private long position; + + /// + /// Initializes a new instance of the class. + /// + /// The containing icon stream. + public IconFrameStream(Stream stream) + => this.stream = stream; + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => true; + + /// + public override bool CanWrite => false; + + /// + public override long Length => this.length; + + /// + public override long Position + { + get => this.position; + set => this.Seek(value, SeekOrigin.Begin); + } + + /// + /// Repositions this stream over another image payload in the same containing stream. + /// + /// The absolute start of the image payload. + /// The image payload length. + public void Reset(long start, long length) + { + this.start = start; + this.length = length; + this.position = 0; + } + + /// + public override void Flush() + { + } + + /// + public override int Read(byte[] buffer, int offset, int count) + => this.Read(buffer.AsSpan(offset, count)); + + /// + public override int Read(Span buffer) + { + // Clamp every read to the entry boundary so a child decoder cannot consume the next resource. + int count = (int)Math.Min(buffer.Length, this.length - this.position); + if (count is 0) + { + return 0; + } + + // The containing stream is shared by all entries, so synchronize its absolute position immediately before reading. + this.stream.Position = this.start + this.position; + int read = this.stream.Read(buffer[..count]); + this.position += read; + + return read; + } + + /// + public override long Seek(long offset, SeekOrigin origin) + { + long target = origin switch + { + SeekOrigin.Begin => offset, + SeekOrigin.Current => this.position + offset, + SeekOrigin.End => this.length + offset, + _ => throw new ArgumentOutOfRangeException(nameof(origin)) + }; + + // Casting rejects both negative offsets and offsets beyond Length with one bounds check. + if ((ulong)target > (ulong)this.length) + { + throw new InvalidImageContentException("The embedded icon resource contains an invalid seek offset."); + } + + // Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resources. + this.position = target; + return target; + } + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); +} diff --git a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs b/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs deleted file mode 100644 index 9e7d22de22..0000000000 --- a/src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Diagnostics.CodeAnalysis; - -namespace SixLabors.ImageSharp.Formats.Icon; - -/// -/// Detects ico file headers. -/// -public class IconImageFormatDetector : IImageFormatDetector -{ - /// - public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size; - - /// - public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) - { - format = this.IsSupportedFileFormat(header) switch - { - true => Ico.IcoFormat.Instance, - false => Cur.CurFormat.Instance, - null => default - }; - - return format is not null; - } - - private bool? IsSupportedFileFormat(ReadOnlySpan header) - { - // There are no magic bytes in the first few bytes of a tga file, - // so we try to figure out if its a valid tga by checking for valid tga header bytes. - if (header.Length < this.HeaderSize) - { - return null; - } - - IconDir dir = IconDir.Parse(header); - if (dir is not { Reserved: 0 } // Should be 0. - or not { Type: IconFileType.ICO or IconFileType.CUR } // Unknown Type. - or { Count: 0 }) - { - return null; - } - - IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); - if (entry is not { Reserved: 0 } // Should be 0. - or { BytesInRes: 0 } // Should not be 0. - || entry.ImageOffset < IconDir.Size + (dir.Count * IconDirEntry.Size)) - { - return null; - } - - if (dir.Type is IconFileType.ICO) - { - if (entry is not { BitCount: 1 or 4 or 8 or 16 or 24 or 32 } or not { Planes: 0 or 1 }) - { - return null; - } - - return true; - } - - return false; - } -} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs index 6e83f5b2b4..75f2c63713 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs @@ -55,7 +55,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder private ArithmeticDecodingTable[] acDecodingTables; // Don't make this a ReadOnlySpan, as the values need to get updated. - private readonly byte[] fixedBin = [113, 0, 0, 0]; + private InlineArray4 fixedBin; private readonly CancellationToken cancellationToken; @@ -194,6 +194,9 @@ public ArithmeticScanDecoder(BufferedReadStream stream, SpectralConverter conver this.spectralConverter = converter; this.cancellationToken = cancellationToken; + // Inline storage is zero-initialized with the decoder; only the arithmetic probability state starts nonzero. + this.fixedBin[0] = 113; + this.c = 0; this.a = 0; this.ct = -16; // Force reading 2 initial bytes to fill C. @@ -233,7 +236,7 @@ public void InitDecodingTables(List arithmeticDecodingT } } - private ref byte GetFixedBinReference() => ref MemoryMarshal.GetArrayDataReference(this.fixedBin); + private ref byte GetFixedBinReference() => ref this.fixedBin[0]; /// public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 7cf90bc235..96c69b1143 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; /// Represents a Huffman coding table containing basic coding data plus tables for accelerated computation. ///
[StructLayout(LayoutKind.Sequential)] -internal unsafe struct HuffmanTable +internal struct HuffmanTable { /// /// Memory workspace buffer size used in ctor. @@ -20,25 +20,25 @@ internal unsafe struct HuffmanTable /// /// Derived from the DHT marker. Contains the symbols, in order of incremental code length. /// - public fixed byte Values[256]; + public InlineArray256 Values; /// /// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to /// ensure terminates. /// - public fixed ulong MaxCode[18]; + public InlineArray18 MaxCode; /// /// Values[] offset for codes of length k ValOffset[k] = Values[] index of 1st symbol of code length /// k, less the smallest code of length k; so given a code of length k, the corresponding symbol is /// Values[code + ValOffset[k]]. /// - public fixed int ValOffset[19]; + public InlineArray19 ValOffset; /// /// Contains the length of bits for the given k value. /// - public fixed byte LookaheadSize[JpegConstants.Huffman.LookupSize]; + public InlineArray256 LookaheadSize; /// /// Lookahead table: indexed by the next bits of @@ -50,7 +50,7 @@ internal unsafe struct HuffmanTable /// bits in the corresponding Huffman code, or + 1 /// if too long. The next 8 bits of each entry contain the symbol. /// - public fixed byte LookaheadValue[JpegConstants.Huffman.LookupSize]; + public InlineArray256 LookaheadValue; /// /// Initializes a new instance of the struct. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 2bb97221cc..1730d59dcb 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -43,7 +43,7 @@ internal sealed class PngEncoderCore : IDisposable /// /// Reusable buffer for writing chunk data. /// - private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly + private InlineArray26 chunkDataBuffer; // mutable struct, don't make readonly /// /// The encoder with options @@ -734,9 +734,9 @@ private void WriteHeaderChunk(Stream stream) filterMethod: 0, interlaceMethod: this.interlaceMode); - header.WriteTo(this.chunkDataBuffer.Span); + header.WriteTo(this.chunkDataBuffer); - this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size); + this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size); } /// @@ -749,9 +749,9 @@ private void WriteAnimationControlChunk(Stream stream, uint framesCount, uint pl { AnimationControl acTL = new(framesCount, playsCount); - acTL.WriteTo(this.chunkDataBuffer.Span); + acTL.WriteTo(this.chunkDataBuffer); - this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size); + this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer, 0, AnimationControl.Size); } /// @@ -820,9 +820,9 @@ private void WritePhysicalChunk(Stream stream, ImageMetadata meta) return; } - PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span); + PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer); - this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size); + this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PngPhysical.Size); } /// @@ -1067,7 +1067,7 @@ private void WriteCicpChunk(Stream stream, ImageMetadata metaData) throw new NotSupportedException("CICP matrix coefficients other than Identity are not supported in PNG"); } - Span outputBytes = this.chunkDataBuffer.Span[..4]; + Span outputBytes = this.chunkDataBuffer[..4]; outputBytes[0] = (byte)metaData.CicpProfile.ColorPrimaries; outputBytes[1] = (byte)metaData.CicpProfile.TransferCharacteristics; outputBytes[2] = (byte)metaData.CicpProfile.MatrixCoefficients; @@ -1222,9 +1222,9 @@ private void WriteGammaChunk(Stream stream) // 4-byte unsigned integer of gamma * 100,000. uint gammaValue = (uint)(this.gamma * 100_000F); - BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue); + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer[..4], gammaValue); - this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); + this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer, 0, 4); } } @@ -1241,7 +1241,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) return; } - Span alpha = this.chunkDataBuffer.Span; + Span alpha = this.chunkDataBuffer; if (pngMetadata.ColorType == PngColorType.Rgb) { if (this.use16Bit) @@ -1251,7 +1251,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); } else { @@ -1260,7 +1260,7 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6); } } else if (pngMetadata.ColorType == PngColorType.Grayscale) @@ -1269,14 +1269,14 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) { L16 l16 = pngMetadata.TransparentColor.Value.ToPixel(); BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); } else { L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); alpha.Clear(); alpha[1] = l8.PackedValue; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2); } } } @@ -1301,9 +1301,9 @@ private FrameControl WriteFrameControlChunk(Stream stream, PngFrameMetadata fram disposalMode: frameMetadata.DisposalMode, blendMode: frameMetadata.BlendMode); - fcTL.WriteTo(this.chunkDataBuffer.Span); + fcTL.WriteTo(this.chunkDataBuffer); - this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size); + this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer, 0, FrameControl.Size); return fcTL; } @@ -1834,12 +1834,4 @@ private static int CalculateBytesPerPixel(PngColorType? pngColorType, bool use16 // PngColorType.RgbWithAlpha _ => use16Bit ? 8 : 4, }; - - private unsafe struct ScratchBuffer - { - private const int Size = 26; - private fixed byte scratch[Size]; - - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); - } } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 421b7cee33..39c4beb618 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -100,7 +100,7 @@ public static WebpVp8X WriteTrunksBeforeData( bool hasAnimation) { // Write file size later - RiffHelper.BeginWriteRiff(stream, WebpConstants.WebpFormTypeFourCc); + RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc); // Write VP8X, header if necessary. WebpVp8X vp8x = default; @@ -151,7 +151,7 @@ public static void WriteTrunksAfterData( RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data); } - RiffHelper.EndWriteVp8X(stream, in vp8x, updateVp8x, initialPosition); + RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition); } /// @@ -189,7 +189,7 @@ public static void WriteAlphaChunk(Stream stream, Span dataBytes, bool alp stream.WriteByte(flags); stream.Write(dataBytes); - RiffHelper.EndWriteChunk(stream, pos, 2); + RiffHelper.EndWriteChunk(stream, pos); } /// diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs index ace2db4bdf..7cec7c1db8 100644 --- a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs +++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs @@ -130,6 +130,6 @@ public void WriteTo(Stream stream) WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1); WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1); - RiffHelper.EndWriteChunk(stream, pos, 2); + RiffHelper.EndWriteChunk(stream, pos); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 6bde77fe13..a051cf0ce8 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -24,7 +24,7 @@ internal class Vp8LEncoder : IDisposable /// /// Scratch buffer to reduce allocations. /// - private ScratchBuffer scratch; // mutable struct, don't make readonly + private InlineArray256 scratch; // mutable struct, don't make readonly private readonly int[][] histoArgb = [new int[256], new int[256], new int[256], new int[256]]; @@ -315,7 +315,7 @@ public bool Encode(ImageFrame frame, Rectangle bounds, WebpFrame if (hasAnimation) { - RiffHelper.EndWriteChunk(stream, prevPosition, 2); + RiffHelper.EndWriteChunk(stream, prevPosition); } return hasAlpha; @@ -803,7 +803,7 @@ private void ApplyCrossColorFilter(int width, int height, bool lowEffort) int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits); int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits); - PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch.Span); + PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch); this.bitWriter.PutBits(WebpConstants.TransformPresent, 1); this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2); @@ -876,7 +876,7 @@ private void EncodeImageNoHuffman(Span bgra, Vp8LHashChain hashChain, Vp8L private void StoreHuffmanCode(Span huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode) { int count = 0; - Span symbols = this.scratch.Span[..2]; + Span symbols = this.scratch[..2]; symbols.Clear(); const int maxBits = 8; const int maxSymbol = 1 << maxBits; @@ -1915,15 +1915,4 @@ public void Dispose() this.HashChain.Dispose(); } - - /// - /// Scratch buffer to reduce allocations. - /// - private unsafe struct ScratchBuffer - { - private const int Size = 256; - private fixed int scratch[Size]; - - public Span Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size); - } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs index ab83e1641c..7548d5d7ea 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -35,7 +35,7 @@ public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Se int lambda = dqm.LambdaI16; int tlambda = dqm.TLambda; Span src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); - Span scratch = it.Scratch3; + Span scratch = it.Scratch3.AsSpan(); Vp8ModeScore rdTmp = new(); Vp8Residual res = new(); Vp8ModeScore rdCur = rdTmp; @@ -105,7 +105,7 @@ public static bool PickBestIntra4(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8Seg int tlambda = dqm.TLambda; Span src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc); Span bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc); - Span scratch = it.Scratch3; + Span scratch = it.Scratch3.AsSpan(); int totalHeaderBits = 0; Vp8ModeScore rdBest = new(); @@ -280,7 +280,7 @@ public static int ReconstructIntra16(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8M int nz = 0; int n; Span shortScratchSpan = it.Scratch2.AsSpan(); - Span scratch = it.Scratch3.AsSpan(0, 16); + Span scratch = it.Scratch3.AsSpan(); shortScratchSpan.Clear(); scratch.Clear(); Span dcTmp = shortScratchSpan[..16]; @@ -321,7 +321,7 @@ public static int ReconstructIntra4(Vp8EncIterator it, Vp8SegmentInfo dqm, Span< { Span reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]); Span tmp = it.Scratch2.AsSpan(0, 16); - Span scratch = it.Scratch3.AsSpan(0, 16); + Span scratch = it.Scratch3.AsSpan(); Vp8Encoding.FTransform(src, reference, tmp, scratch); int nz = QuantizeBlock(tmp, levels, ref dqm.Y1); Vp8Encoding.ITransformOne(reference, tmp, yuvOut, scratch); @@ -336,7 +336,7 @@ public static int ReconstructUv(Vp8EncIterator it, Vp8SegmentInfo dqm, Vp8ModeSc int nz = 0; int n; Span tmp = it.Scratch2.AsSpan(0, 8 * 16); - Span scratch = it.Scratch3.AsSpan(0, 16); + Span scratch = it.Scratch3.AsSpan(); for (n = 0; n < 8; n += 2) { diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs index 3c8bafa1b2..efb1ce0c05 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs @@ -13,8 +13,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy; /// internal class Vp8Decoder : IDisposable { + private const int UpsamplingBufferSize = (14 * 32) + 15; + private Vp8MacroBlock leftMacroBlock; + // Vector upsampling needs 15 bytes of left padding, 128 interleaved UV bytes, two 128-byte BGR rows, and two 32-byte luma rows. + private readonly IMemoryOwner upsamplingBuffer; + /// /// Initializes a new instance of the class. /// @@ -74,6 +79,7 @@ public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp this.TmpYBuffer = memoryAllocator.Allocate((int)width); this.TmpUBuffer = memoryAllocator.Allocate((int)width); this.TmpVBuffer = memoryAllocator.Allocate((int)width); + this.upsamplingBuffer = memoryAllocator.Allocate(UpsamplingBufferSize); this.Pixels = memoryAllocator.Allocate((int)(width * height * 4), AllocationOptions.Clean); #if DEBUG @@ -238,6 +244,11 @@ public Vp8Decoder(Vp8FrameHeader frameHeader, Vp8PictureHeader pictureHeader, Vp /// public IMemoryOwner Pixels { get; } + /// + /// Gets the reusable workspace used while upsampling YUV rows. + /// + public Span UpsamplingBuffer => this.upsamplingBuffer.Memory.Span[..UpsamplingBufferSize]; + /// /// Gets or sets filter info. /// @@ -339,6 +350,7 @@ public void Dispose() this.TmpYBuffer.Dispose(); this.TmpUBuffer.Dispose(); this.TmpVBuffer.Dispose(); + this.upsamplingBuffer.Dispose(); this.Pixels.Dispose(); } } diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 6725b97d8d..85739a3e20 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -517,7 +517,7 @@ private bool Encode( if (hasAnimation) { - RiffHelper.EndWriteChunk(stream, prevPosition, 2); + RiffHelper.EndWriteChunk(stream, prevPosition); } } finally diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs index 5cd8812c95..e602c43551 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy; -internal unsafe struct Vp8Matrix +internal struct Vp8Matrix { // [luma-ac,luma-dc,chroma][dc,ac] private static readonly int[][] BiasMatrices = @@ -21,27 +21,27 @@ internal unsafe struct Vp8Matrix /// /// The quantizer steps. /// - public fixed ushort Q[16]; + public InlineArray16 Q; /// /// The reciprocals, fixed point. /// - public fixed ushort IQ[16]; + public InlineArray16 IQ; /// /// The rounding bias. /// - public fixed uint Bias[16]; + public InlineArray16 Bias; /// /// The value below which a coefficient is zeroed. /// - public fixed uint ZThresh[16]; + public InlineArray16 ZThresh; /// /// The frequency boosters for slight sharpening. /// - public fixed short Sharpen[16]; + public InlineArray16 Sharpen; // Sharpening by (slightly) raising the hi-frequency coeffs. // Hack-ish but helpful for mid-bitrate range. Use with care. diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index f14df853cd..bff13d4020 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -733,7 +733,7 @@ private static int EmitRgb(Vp8Decoder dec, Vp8Io io) int mbw = io.MbW; int uvw = (mbw + 1) >> 1; // >> 1 is bit-hack for / 2 int y = io.MbY; - byte[] uvBuffer = new byte[(14 * 32) + 15]; + Span uvBuffer = dec.UpsamplingBuffer; if (y == 0) { diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index d5f91b7c88..a1fa520725 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -27,7 +27,7 @@ internal static class YuvConversion // we interpolate u/v as: // ([9*a + 3*b + 3*c + d 3*a + 9*b + 3*c + d] + [8 8]) / 16 // ([3*a + b + 9*c + 3*d a + 3*b + 3*c + 9*d] [8 8]) / 16 - public static void UpSample(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, byte[] uvBuffer) + public static void UpSample(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, Span uvBuffer) { if (Vector128.IsHardwareAccelerated) { @@ -107,11 +107,11 @@ private static void UpSampleScalar(Span topY, Span bottomY, Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, byte[] uvBuffer) + private static void UpSampleVector128(Span topY, Span bottomY, Span topU, Span topV, Span curU, Span curV, Span topDst, Span bottomDst, int len, Span uvBuffer) { const int xStep = 3; - Array.Clear(uvBuffer); - Span ru = uvBuffer.AsSpan(15); + uvBuffer.Clear(); + Span ru = uvBuffer[15..]; Span rv = ru[32..]; // Treat the first pixel in regular way. diff --git a/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs b/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs deleted file mode 100644 index 3fd67e5359..0000000000 --- a/src/ImageSharp/Formats/Webp/RiffChunkHeader.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal readonly struct RiffChunkHeader -{ - public readonly uint FourCc; - - public readonly uint Size; - - public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); -} diff --git a/src/ImageSharp/Formats/Webp/RiffHelper.cs b/src/ImageSharp/Formats/Webp/RiffHelper.cs index 1a409eb8e0..b6318c7486 100644 --- a/src/ImageSharp/Formats/Webp/RiffHelper.cs +++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs @@ -2,93 +2,137 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; +using System.Text; using SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp; internal static class RiffHelper { - public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) - { - long pos = BeginWriteChunk(stream, fourCc); - stream.Write(data); - EndWriteChunk(stream, pos); - } + /// + /// The header bytes identifying RIFF file. + /// + private const uint RiffFourCc = 0x52_49_46_46; - public static void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) - where TStruct : unmanaged => - WriteChunk(stream, fourCc, MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in chunk, 1))); + public static void WriteRiffFile(Stream stream, string formType, Action func) => + WriteChunk(stream, RiffFourCc, s => + { + s.Write(Encoding.ASCII.GetBytes(formType)); + func(s); + }); - public static long BeginWriteChunk(Stream stream, ReadOnlySpan fourCc) + public static void WriteChunk(Stream stream, uint fourCc, Action func) { + Span buffer = stackalloc byte[4]; + // write the fourCC - stream.Write(fourCc); + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); long sizePosition = stream.Position; - - // Leaving the place for the size stream.Position += 4; - return sizePosition; + func(stream); + + long position = stream.Position; + + uint dataSize = (uint)(position - sizePosition - 4); + + // padding + if (dataSize % 2 == 1) + { + stream.WriteByte(0); + position++; + } + + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); + stream.Position = sizePosition; + stream.Write(buffer); + stream.Position = position; } - public static long BeginWriteChunk(Stream stream, uint fourCc) + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) { Span buffer = stackalloc byte[4]; + + // write the fourCC BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - return BeginWriteChunk(stream, buffer); + stream.Write(buffer); + uint size = (uint)data.Length; + BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); + stream.Write(buffer); + stream.Write(data); + + // padding + if (size % 2 is 1) + { + stream.WriteByte(0); + } } - public static long BeginWriteRiff(Stream stream, ReadOnlySpan formType) + public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged { - long sizePosition = BeginWriteChunk(stream, "RIFF"u8); - stream.Write(formType); - return sizePosition; + fixed (TStruct* ptr = &chunk) + { + WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); + } } - public static long BeginWriteList(Stream stream, ReadOnlySpan listType) + public static long BeginWriteChunk(Stream stream, uint fourCc) { - long sizePosition = BeginWriteChunk(stream, "LIST"u8); - stream.Write(listType); + Span buffer = stackalloc byte[4]; + + // write the fourCC + BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); + stream.Write(buffer); + + long sizePosition = stream.Position; + stream.Position += 4; + return sizePosition; } - public static void EndWriteChunk(Stream stream, long sizePosition, int alignment = 1) + public static void EndWriteChunk(Stream stream, long sizePosition) { - Guard.MustBeGreaterThan(alignment, 0, nameof(alignment)); + Span buffer = stackalloc byte[4]; - long currentPosition = stream.Position; + long position = stream.Position; - uint dataSize = (uint)(currentPosition - sizePosition - 4); + uint dataSize = (uint)(position - sizePosition - 4); - // Add padding - while (dataSize % alignment is not 0) + // padding + if (dataSize % 2 is 1) { stream.WriteByte(0); - dataSize++; - currentPosition++; + position++; } // Add the size of the encoded file to the Riff header. + BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); stream.Position = sizePosition; - stream.Write(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref dataSize), sizeof(uint))); - stream.Position = currentPosition; + stream.Write(buffer); + stream.Position = position; + } + + public static long BeginWriteRiffFile(Stream stream, string formType) + { + long sizePosition = BeginWriteChunk(stream, RiffFourCc); + stream.Write(Encoding.ASCII.GetBytes(formType)); + return sizePosition; } - public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X, long initPosition) + public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition) { - // Jump through "RIFF" fourCC - EndWriteChunk(stream, initPosition + 4, 2); + EndWriteChunk(stream, sizePosition + 4); // Write the VP8X chunk if necessary. - if (updateVp8X) + if (updateVp8x) { long position = stream.Position; - stream.Position = initPosition + 12; - vp8X.WriteTo(stream); + stream.Position = sizePosition + 12; + vp8x.WriteTo(stream); stream.Position = position; } } diff --git a/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs b/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs deleted file mode 100644 index 4f12d8342e..0000000000 --- a/src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace SixLabors.ImageSharp.Formats.Webp; - -internal readonly struct RiffOrListChunkHeader -{ - public const int HeaderSize = 12; - - public readonly uint FourCc; - - public readonly uint Size; - - public readonly uint FormType; - - public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); - - public bool IsRiff => this.FourCc is 0x52_49_46_46; // "RIFF" - - public bool IsList => this.FourCc is 0x4C_49_53_54; // "LIST" - - public static ref RiffOrListChunkHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); -} diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 7bee3c8c50..f489899c68 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -28,11 +28,6 @@ internal static class WebpConstants 0x2A ]; - /// - /// Gets the header bytes identifying a Webp. - /// - public const uint WebpFourCc = 0x57_45_42_50; - /// /// Signature byte which identifies a VP8L header. /// @@ -60,6 +55,11 @@ internal static class WebpConstants 0x50 // P ]; + /// + /// The header bytes identifying a Webp. + /// + public const string WebpFourCc = "WEBP"; + /// /// 3 bits reserved for version. /// @@ -319,9 +319,4 @@ internal static class WebpConstants -7, 8, -8, -9 ]; - - /// - /// Gets the header bytes identifying a Webp. - /// - public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; } diff --git a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs index a7f8d43672..2b91aa95fe 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => RiffOrListChunkHeader.HeaderSize; + public int HeaderSize => 12; /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) @@ -21,9 +21,21 @@ public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out I } private bool IsSupportedFileFormat(ReadOnlySpan header) - => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is - { - IsRiff: true, - FormType: WebpConstants.WebpFourCc - }; + => header.Length >= this.HeaderSize && IsRiffContainer(header) && IsWebpFile(header); + + /// + /// Checks, if the header starts with a valid RIFF FourCC. + /// + /// The header bytes. + /// True, if its a valid RIFF FourCC. + private static bool IsRiffContainer(ReadOnlySpan header) + => header[..4].SequenceEqual(WebpConstants.RiffFourCc); + + /// + /// Checks if 'WEBP' is present in the header. + /// + /// The header bytes. + /// True, if its a webp file. + private static bool IsWebpFile(ReadOnlySpan header) + => header.Slice(8, 4).SequenceEqual(WebpConstants.WebpHeader); } diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index fa6eaa722d..3c459eca3f 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. // +using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; @@ -22,6 +23,108 @@ namespace SixLabors.ImageSharp; /// public static partial class ImageExtensions { + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsAni(this Image source, string path) => SaveAsAni(source, path, default); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsAniAsync(this Image source, string path) => SaveAsAniAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsAniAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsAniAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsAni(this Image source, string path, AniEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsAniAsync(this Image source, string path, AniEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsAni(this Image source, Stream stream) + => SaveAsAni(source, stream, default); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsAniAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsAniAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsAni(this Image source, Stream stream, AniEncoder encoder) + => source.Save( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance)); + + /// + /// Saves the image to the given stream with the Ani format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsAniAsync(this Image source, Stream stream, AniEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance), + cancellationToken); + /// /// Saves the image to the given stream with the Bmp format. /// diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index 5f31daebcb..4a038b5fce 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -3,6 +3,7 @@ // using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Gif; @@ -14,7 +15,6 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Exr; namespace SixLabors.ImageSharp; @@ -24,6 +24,26 @@ namespace SixLabors.ImageSharp; /// public static class ImageMetadataExtensions { + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -104,26 +124,6 @@ public static class ImageMetadataExtensions /// The new public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); - /// - /// Gets the from .
- /// If none is found, an instance is created either by conversion from the decoded image format metadata - /// or the requested format default constructor. - /// This instance will be added to the metadata for future requests. - ///
- /// The image metadata. - /// - /// The - /// - public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance); - - /// - /// Creates a new cloned instance of from the . - /// The instance is created via - /// - /// The image metadata. - /// The new - public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); - /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata @@ -286,47 +286,47 @@ public static class ImageMetadataExtensions /// - /// Gets the from .
+ /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// or the requested format default constructor. /// This instance will be added to the metadata for future requests. ///
/// The image frame metadata. /// - /// The + /// The /// - public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance); + public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance); /// - /// Creates a new cloned instance of from the . - /// The instance is created via + /// Creates a new cloned instance of from the . + /// The instance is created via /// /// The image frame metadata. - /// The new - public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); + /// The new + public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); /// - /// Gets the from .
+ /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// or the requested format default constructor. /// This instance will be added to the metadata for future requests. ///
/// The image frame metadata. /// - /// The + /// The /// - public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); + public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance); /// - /// Creates a new cloned instance of from the . - /// The instance is created via + /// Creates a new cloned instance of from the . + /// The instance is created via /// /// The image frame metadata. - /// The new - public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// The new + public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance); /// - /// Gets the from .
+ /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata /// or the requested format default constructor. /// This instance will be added to the metadata for future requests. @@ -335,15 +335,15 @@ public static class ImageMetadataExtensions /// /// The /// - public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance); + public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance); /// /// Creates a new cloned instance of from the . - /// The instance is created via + /// The instance is created via /// /// The image frame metadata. /// The new - public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); + public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); /// /// Gets the from .
diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude index c1c69c5b5b..e8077c2939 100644 --- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -4,6 +4,7 @@ // Licensed under the Six Labors Split License. <#+ private static readonly string[] formats = [ + "Ani", "Bmp", "Cur", "Gif", @@ -19,6 +20,7 @@ ]; private static readonly string[] frameFormats = [ + "Ani", "Cur", "Ico", "Gif", diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 3642017e3f..8080aab87f 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.IO; @@ -23,14 +22,10 @@ internal sealed class BufferedReadStream : Stream private readonly unsafe byte* pinnedReadBuffer; - /// - /// Index within our buffer, not reader position. - /// + // Index within our buffer, not reader position. private int readBufferIndex; - /// - /// Matches what the stream position would be without buffering - /// + // Matches what the stream position would be without buffering private long readerPosition; private bool isDisposed; @@ -199,49 +194,6 @@ public override int Read(Span buffer) return this.ReadToBufferViaCopyFast(buffer); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool TryReadUnmanaged(out T result) - where T : unmanaged - { - this.cancellationToken.ThrowIfCancellationRequested(); - - int size = Unsafe.SizeOf(); - - if (size > this.BufferSize) - { - Span span = stackalloc byte[size]; - if (this.ReadToBufferDirectSlow(span) != size) - { - result = default; - return false; - } - - result = MemoryMarshal.Read(span); - } - else - { - if ((uint)this.readBufferIndex > (uint)(this.BufferSize - size)) - { - this.FillReadBuffer(); - } - - if (this.GetCopyCount(size) != size) - { - this.EofHitCount++; - result = default; - return false; - } - - Span span = this.readBuffer.AsSpan(this.readBufferIndex, size); - - this.readerPosition += size; - this.readBufferIndex += size; - result = MemoryMarshal.Read(span); - } - - return true; - } - /// public override void Flush() { diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 71f2bc333c..da57541faa 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -19,7 +19,11 @@ - + + diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index bd65200844..ac1a18cf8e 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 = 12; + private readonly int expectedDefaultConfigurationCount = 13; public ConfigurationTests() { diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs index 17439e3092..bc5e4ebb82 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Ani; @@ -11,12 +13,136 @@ namespace SixLabors.ImageSharp.Tests.Formats.Ani; [ValidateDisposedMemoryAllocations] public class AniDecoderTests { + /// + /// Verifies that ANI animation steps and embedded CUR resolution variants are flattened with their ANI metadata. + /// [Theory] - [WithFile(Work, PixelTypes.Rgba32)] - [WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)] - [WithFile(Help, PixelTypes.Rgba32)] - public void AniDecoder_Decode(TestImageProvider provider) + [WithFile(Work, PixelTypes.Rgba32, 17, 1, 6U, 6U)] + [WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32, 54, 3, 3U, 3U)] + [WithFile(Help, PixelTypes.Rgba32, 4, 1, 10U, 12U)] + public void AniDecoder_Decode( + TestImageProvider provider, + int expectedFrameCount, + int variantsPerStep, + uint expectedDisplayRate, + uint expectedFrameDelay) { using Image image = provider.GetImage(AniDecoder.Instance); + + Assert.Equal(expectedFrameCount, image.Frames.Count); + Assert.Equal(expectedDisplayRate, image.Metadata.GetAniMetadata().DisplayRate); + + for (int i = 0; i < image.Frames.Count; i++) + { + AniFrameMetadata metadata = image.Frames[i].Metadata.GetAniMetadata(); + + Assert.Equal((i / variantsPerStep) + 1, metadata.SequenceNumber); + Assert.Equal(expectedFrameDelay, metadata.FrameDelay); + Assert.Equal(AniFrameFormat.Cur, metadata.FrameFormat); + Assert.NotEqual(0, (int)metadata.BmpBitsPerPixel); + } + } + + /// + /// Verifies that identification exposes the same flattened ANI frame structure without decoding pixels. + /// + [Theory] + [InlineData(Work, 17)] + [InlineData(MultiFramesInEveryIconChunk, 54)] + [InlineData(Help, 4)] + public void AniDecoder_Identify(string path, int expectedFrameCount) + { + TestFile file = TestFile.Create(path); + using MemoryStream stream = new(file.Bytes, false); + + ImageInfo info = AniDecoder.Instance.Identify(DecoderOptions.Default, stream); + + Assert.Equal(expectedFrameCount, info.FrameMetadataCollection.Count); + + for (int i = 0; i < info.FrameMetadataCollection.Count; i++) + { + Assert.True(info.FrameMetadataCollection[i].GetAniMetadata().SequenceNumber > 0); + } + } + + /// + /// Verifies that invalid sequence metadata follows the ancillary-segment integrity policy. + /// + [Fact] + public void AniDecoder_InvalidSequenceReference_FollowsIntegrityHandling() + { + byte[] data = TestFile.Create(Help).Bytes.ToArray(); + int sequenceOffset = data.AsSpan().IndexOf("seq "u8); + Assert.True(sequenceOffset >= 0); + + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sequenceOffset + AniConstants.ChunkHeaderSize), uint.MaxValue); + + using MemoryStream strictStream = new(data, false); + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); + + using MemoryStream ignoreStream = new(data, false); + DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreAncillary }; + using Image image = AniDecoder.Instance.Decode(ignore, ignoreStream); + + Assert.Equal(3, image.Frames.Count); + } + + /// + /// Verifies that ignored corrupt resources retain their sequence-table slot. + /// + [Fact] + public void AniDecoder_UnsupportedResource_FollowsIntegrityHandling() + { + byte[] data = TestFile.Create(Work).Bytes.ToArray(); + int resourceOffset = data.AsSpan().IndexOf("icon"u8); + Assert.True(resourceOffset >= 0); + + int iconTypeOffset = resourceOffset + AniConstants.ChunkHeaderSize + sizeof(ushort); + BinaryPrimitives.WriteUInt16LittleEndian(data.AsSpan(iconTypeOffset), ushort.MaxValue); + + using MemoryStream strictStream = new(data, false); + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); + + using MemoryStream ignoreStream = new(data, false); + DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + using Image image = AniDecoder.Instance.Decode(ignore, ignoreStream); + + Assert.Equal(16, image.Frames.Count); + Assert.Equal(2, image.Frames.RootFrame.Metadata.GetAniMetadata().SequenceNumber); + } + + /// + /// Verifies that a malformed rate chunk follows the ancillary-segment integrity policy. + /// + [Fact] + public void AniDecoder_InvalidRateChunk_FollowsIntegrityHandling() + { + byte[] source = TestFile.Create(Help).Bytes.ToArray(); + int rateOffset = source.AsSpan().IndexOf("rate"u8); + Assert.True(rateOffset >= 0); + + int rateSizeOffset = rateOffset + sizeof(uint); + int rateSize = (int)BinaryPrimitives.ReadUInt32LittleEndian(source.AsSpan(rateSizeOffset)); + int rateEnd = rateOffset + AniConstants.ChunkHeaderSize + rateSize; + byte[] data = new byte[source.Length + 2]; + source.AsSpan(0, rateEnd).CopyTo(data); + source.AsSpan(rateEnd).CopyTo(data.AsSpan(rateEnd + 2)); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(rateSizeOffset), (uint)rateSize + 2); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - 8); + + using MemoryStream defaultStream = new(data, false); + using Image image = AniDecoder.Instance.Decode(DecoderOptions.Default, defaultStream); + + Assert.Equal(4, image.Frames.Count); + foreach (ImageFrame frame in image.Frames) + { + Assert.Equal(10U, frame.Metadata.GetAniMetadata().FrameDelay); + } + + using MemoryStream strictStream = new(data, false); + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); } } diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs new file mode 100644 index 0000000000..8f905a093e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Ani; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using static SixLabors.ImageSharp.Tests.TestImages.Ani; + +namespace SixLabors.ImageSharp.Tests.Formats.Ani; + +[Trait("Format", "Ani")] +[ValidateDisposedMemoryAllocations] +public class AniEncoderTests +{ + /// + /// Verifies that ANI resources, including multi-resolution CUR resources, survive an encode/decode round trip. + /// + [Theory] + [WithFile(Work, PixelTypes.Rgba32)] + [WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)] + [WithFile(Help, PixelTypes.Rgba32)] + public void AniEncoder_RoundTrips(TestImageProvider provider) + { + using Image image = provider.GetImage(AniDecoder.Instance); + using MemoryStream stream = new(); + + image.Save(stream, new AniEncoder()); + + // The RIFF size covers everything after its identifier and size field. + Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + + ImageComparer.Exact.VerifySimilarity(image, decoded); + Assert.Equal(image.Frames.Count, decoded.Frames.Count); + + for (int i = 0; i < image.Frames.Count; i++) + { + AniFrameMetadata expected = image.Frames[i].Metadata.GetAniMetadata(); + AniFrameMetadata actual = decoded.Frames[i].Metadata.GetAniMetadata(); + + Assert.Equal(expected.SequenceNumber, actual.SequenceNumber); + Assert.Equal(expected.FrameDelay, actual.FrameDelay); + Assert.Equal(expected.FrameFormat, actual.FrameFormat); + Assert.Equal(expected.EncodingWidth, actual.EncodingWidth); + Assert.Equal(expected.EncodingHeight, actual.EncodingHeight); + Assert.Equal(expected.Compression, actual.Compression); + Assert.Equal(expected.BmpBitsPerPixel, actual.BmpBitsPerPixel); + Assert.Equal(expected.HotspotX, actual.HotspotX); + Assert.Equal(expected.HotspotY, actual.HotspotY); + Assert.Equal(expected.ColorTable?.ToArray(), actual.ColorTable?.ToArray()); + } + } + + /// + /// Verifies that per-step rates and RIFF information metadata are emitted and decoded. + /// + [Theory] + [WithFile(Work, PixelTypes.Rgba32)] + public void AniEncoder_WritesVariableRatesAndInformation(TestImageProvider provider) + { + using Image image = provider.GetImage(AniDecoder.Instance); + AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); + imageMetadata.Name = "ImageSharp ANI"; + imageMetadata.Artist = "Six Labors"; + + for (int i = 0; i < image.Frames.Count; i++) + { + image.Frames[i].Metadata.GetAniMetadata().FrameDelay = (uint)(i + 1); + } + + using MemoryStream stream = new(); + image.Save(stream, new AniEncoder()); + + Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + AniMetadata decodedMetadata = decoded.Metadata.GetAniMetadata(); + + Assert.Equal(imageMetadata.Name, decodedMetadata.Name); + Assert.Equal(imageMetadata.Artist, decodedMetadata.Artist); + + for (int i = 0; i < decoded.Frames.Count; i++) + { + Assert.Equal((uint)(i + 1), decoded.Frames[i].Metadata.GetAniMetadata().FrameDelay); + } + } + + /// + /// Verifies the encoder and decoder paths for embedded ICO and BMP resources. + /// + [Theory] + [InlineData(AniFrameFormat.Ico)] + [InlineData(AniFrameFormat.Bmp)] + public void AniEncoder_RoundTripsOtherFrameFormats(AniFrameFormat frameFormat) + { + using Image image = new(16, 16, Color.Red.ToPixel()); + AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); + imageMetadata.DisplayRate = 6; + imageMetadata.BitCount = 32; + imageMetadata.Planes = 1; + + AniFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); + frameMetadata.FrameDelay = 6; + frameMetadata.SequenceNumber = 1; + frameMetadata.FrameFormat = frameFormat; + frameMetadata.Compression = IconFrameCompression.Bmp; + + using MemoryStream stream = new(); + image.Save(stream, new AniEncoder()); + + Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); + + if (frameFormat is AniFrameFormat.Bmp) + { + ReadOnlySpan encoded = stream.GetBuffer().AsSpan(0, (int)stream.Length); + int frameChunkOffset = encoded.IndexOf("icon"u8); + Assert.True(frameChunkOffset >= 0); + + // AF_ICON-clear resources contain a headerless BMP DIB, not a standalone file beginning with BITMAPFILEHEADER. + ReadOnlySpan frameData = encoded[(frameChunkOffset + AniConstants.ChunkHeaderSize)..]; + Assert.False(frameData.StartsWith("BM"u8)); + } + + stream.Position = 0; + using Image decoded = Image.Load(stream); + + ImageComparer.Exact.VerifySimilarity(image, decoded); + Assert.Equal(frameFormat, decoded.Frames.RootFrame.Metadata.GetAniMetadata().FrameFormat); + } + + /// + /// Verifies that an independent frame cannot collide with an explicit sequence group. + /// + [Fact] + public void AniEncoder_NonPositiveSequenceDoesNotCollideWithExplicitGroup() + { + using Image image = new(16, 16, Color.Red.ToPixel()); + image.Frames.AddFrame(image.Frames.RootFrame); + image.Frames[1].Metadata.GetAniMetadata().SequenceNumber = 1; + + using MemoryStream stream = new(); + image.Save(stream, new AniEncoder()); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + + Assert.Equal(2, decoded.Frames.Count); + Assert.Equal(1, decoded.Frames[0].Metadata.GetAniMetadata().SequenceNumber); + Assert.Equal(2, decoded.Frames[1].Metadata.GetAniMetadata().SequenceNumber); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs new file mode 100644 index 0000000000..676556827a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Ani; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Tests.Formats.Ani; + +[Trait("Format", "Ani")] +public class AniMetadataTests +{ + /// + /// Verifies that resizing scales the ANI-owned encoding dimensions exactly once. + /// + [Fact] + public void AfterFrameApply_ScalesEncodingDimensionsOnce() + { + using Image image = new(32, 32); + AniFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); + metadata.EncodingWidth = 32; + metadata.EncodingHeight = 32; + metadata.FrameFormat = AniFrameFormat.Cur; + + image.Mutate(context => context.Resize(64, 64)); + + AniFrameMetadata resized = image.Frames.RootFrame.Metadata.GetAniMetadata(); + Assert.Equal((byte)64, resized.EncodingWidth); + Assert.Equal((byte)64, resized.EncodingHeight); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs index bac52fc728..80d721ba9f 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Icon; @@ -13,6 +14,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur; [ValidateDisposedMemoryAllocations] public class CurDecoderTests { + [Fact] + public void CurFormat_HasCorrectName() + => Assert.Equal("CUR", CurFormat.Instance.Name); + + [Fact] + public void CurDetector_RejectsIco() + { + TestFile file = TestFile.Create(TestImages.Ico.Flutter); + CurImageFormatDetector detector = new(); + + Assert.False(detector.TryDetectFormat(file.Bytes, out _)); + } + [Theory] [WithFile(WindowsMouse, PixelTypes.Rgba32)] public void CurDecoder_Decode(TestImageProvider provider) @@ -38,4 +52,30 @@ public void CurDecoder_Decode2(TestImageProvider provider) Assert.Equal(IconFrameCompression.Bmp, meta.Compression); Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel); } + + [Fact] + public void CurFrameMetadata_DeepClonePreservesColorTable() + { + Color[] colors = [Color.Red, Color.Green]; + CurFrameMetadata metadata = new() { ColorTable = colors }; + + CurFrameMetadata clone = metadata.DeepClone(); + colors[0] = Color.Blue; + + Assert.Equal(Color.Red, clone.ColorTable.Value.Span[0]); + Assert.Equal(Color.Green, clone.ColorTable.Value.Span[1]); + } + + [Fact] + public void CurFrameMetadata_ScalesZeroEncodingDimensionsFrom256() + { + using Image source = new(256, 256); + using Image destination = new(128, 128); + CurFrameMetadata metadata = new() { EncodingWidth = 0, EncodingHeight = 0 }; + + metadata.AfterFrameApply(source.Frames.RootFrame, destination.Frames.RootFrame, Matrix4x4.Identity); + + Assert.Equal((byte)128, metadata.EncodingWidth); + Assert.Equal((byte)128, metadata.EncodingHeight); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs index c41f455cd1..305adb9895 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs @@ -2,9 +2,12 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Quantization; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; using static SixLabors.ImageSharp.Tests.TestImages.Ico; @@ -124,4 +127,37 @@ public void Encode_WithTransparentColorBehaviorClear_Works() } }); } + + [Fact] + public void Encode_UsesBitmapDepthInsteadOfHotspotForQuantizerSelection() + { + using Image image = new(32, 1); + for (int x = 0; x < image.Width; x++) + { + image[x, 0] = new Rgba32((byte)(x * 8), (byte)(255 - (x * 8)), (byte)(x * 4)); + } + + CurFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetCurMetadata(); + metadata.Compression = IconFrameCompression.Bmp; + metadata.BmpBitsPerPixel = BmpBitsPerPixel.Bit8; + metadata.HotspotY = 32; + + CurEncoder encoder = new() + { + Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = 2 }) + }; + + using MemoryStream stream = new(); + image.Save(stream, encoder); + stream.Position = 0; + + using Image decoded = Image.Load(stream); + HashSet colors = []; + for (int x = 0; x < decoded.Width; x++) + { + colors.Add(decoded[x, 0]); + } + + Assert.InRange(colors.Count, 1, 2); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index 539826799e..0329f0e4dd 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers.Binary; +using System.Numerics; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Ico; @@ -14,6 +16,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico; [ValidateDisposedMemoryAllocations] public class IcoDecoderTests { + [Fact] + public void IcoDetector_RejectsCur() + { + TestFile file = TestFile.Create(TestImages.Cur.WindowsMouse); + IcoImageFormatDetector detector = new(); + + Assert.False(detector.TryDetectFormat(file.Bytes, out _)); + } + [Theory] [WithFile(Flutter, PixelTypes.Rgba32)] public void IcoDecoder_Decode(TestImageProvider provider) @@ -306,6 +317,43 @@ public void MultiSize_CanIdentifySingleFrame(string imagePath) Assert.Single(imageInfo.FrameMetadataCollection); } + [Fact] + public void TruncatedEntry_FollowsImageDataIntegrityHandling() + { + byte[] data = TestFile.Create(Flutter).Bytes.ToArray(); + ushort entryCount = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(4)); + Assert.True(entryCount > 1); + + // Limit the first resource below the PNG signature length. A bounded child decoder must not read into following data. + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(IconDir.Size + 8), 4); + + using MemoryStream defaultStream = new(data, false); + Assert.Throws(() => IcoDecoder.Instance.Decode(DecoderOptions.Default, defaultStream)); + + DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using MemoryStream decodeStream = new(data, false); + using Image image = IcoDecoder.Instance.Decode(ignore, decodeStream); + Assert.Equal(entryCount - 1, image.Frames.Count); + + using MemoryStream identifyStream = new(data, false); + ImageInfo info = IcoDecoder.Instance.Identify(ignore, identifyStream); + Assert.Equal(entryCount - 1, info.FrameMetadataCollection.Count); + } + + [Fact] + public void IcoFrameMetadata_ScalesZeroEncodingDimensionsFrom256() + { + using Image source = new(256, 256); + using Image destination = new(128, 128); + IcoFrameMetadata metadata = new() { EncodingWidth = 0, EncodingHeight = 0 }; + + metadata.AfterFrameApply(source.Frames.RootFrame, destination.Frames.RootFrame, Matrix4x4.Identity); + + Assert.Equal((byte)128, metadata.EncodingWidth); + Assert.Equal((byte)128, metadata.EncodingHeight); + } + [Theory] [WithFile(MultiSizeMultiBitsA, PixelTypes.Rgba32)] [WithFile(MultiSizeMultiBitsB, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 9c8f45f784..1cd03e0d5d 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -3,6 +3,7 @@ using Moq; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Jpeg; @@ -30,6 +31,7 @@ public ImageFormatManagerTests() [Fact] public void IfAutoLoadWellKnownFormatsIsTrueAllFormatsAreLoaded() { + Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); @@ -39,6 +41,7 @@ public void IfAutoLoadWellKnownFormatsIsTrueAllFormatsAreLoaded() Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); + Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); From e9aa4bffbb0a6bc6f6ca1828eb2cef60129d4bbc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 27 Jul 2026 13:15:16 +1000 Subject: [PATCH 19/20] Address ANI and icon review feedback --- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 125 ++++++++++++------ src/ImageSharp/Formats/Ani/AniEncoderCore.cs | 50 ++++++- src/ImageSharp/Formats/Ani/AniMetadata.cs | 6 +- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 11 +- .../Formats/Icon/IconDecoderCore.cs | 25 +++- .../Formats/Icon/IconEncoderCore.cs | 115 ++++++++++------ .../Formats/Ani/AniDecoderTests.cs | 2 +- .../Formats/Ani/AniEncoderTests.cs | 86 ++++++++++++ .../Formats/Icon/Ico/IcoEncoderTests.cs | 20 +++ 9 files changed, 338 insertions(+), 102 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 5015efc255..39583cfe4b 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -310,7 +310,7 @@ private void ReadAniHeader(BufferedReadStream stream, uint chunkSize) ReadExactly(stream, data, "ANI header"); this.header = AniHeader.Parse(data); - if (this.header.BytesInHeader < AniHeader.Size) + if (this.header.BytesInHeader < AniHeader.Size || this.header.BytesInHeader > chunkSize) { throw new InvalidImageContentException("The ANI animation header declares an invalid size."); } @@ -412,6 +412,12 @@ private void ReadUInt32Values(BufferedReadStream stream, uint chunkSize, string } int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames); + if (count is 0) + { + this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk does not contain any values."); + return; + } + IMemoryOwner valuesOwner; bool replaceOwner; @@ -532,71 +538,102 @@ private void ProcessFrameChunks(BufferedReadStream stream, List<(AniFrameForm bool hasSequence = this.sequence is not null; int decodedResourceCount = 0; int maxDecodedResources = (int)this.Options.MaxFrames; - uint lastRequiredResource = 0; + IMemoryOwner? sortedSequenceOwner = null; - if (hasSequence) + try { - if (sequence.IsEmpty) + ReadOnlySpan requiredResources = sequence; + if (hasSequence) { - return; - } + bool isSorted = true; + for (int i = 1; i < sequence.Length; i++) + { + if (sequence[i] < sequence[i - 1]) + { + isSorted = false; + break; + } + } - // Only resources referenced by the retained sequence steps can contribute to the bounded output frame set. - for (int i = 0; i < sequence.Length; i++) - { - lastRequiredResource = Math.Max(lastRequiredResource, sequence[i]); + if (!isSorted) + { + // Playback order can reference resources arbitrarily. A sorted allocator-owned copy turns the physical + // resource scan into a linear merge instead of searching the complete sequence for every icon chunk. + sortedSequenceOwner = this.Options.Configuration.MemoryAllocator.Allocate(sequence.Length); + Span sortedSequence = sortedSequenceOwner.GetSpan(); + sequence.CopyTo(sortedSequence); + sortedSequence.Sort(); + requiredResources = sortedSequence; + } } - } - foreach ((long start, long end) in this.frameLists) - { - stream.Position = start; + int requiredResourceIndex = 0; + uint lastRequiredResource = hasSequence ? requiredResources[^1] : 0; - while (stream.Position + AniConstants.ChunkHeaderSize <= end) + foreach ((long start, long end) in this.frameLists) { - AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); - long dataStart = stream.Position; - long dataEnd = GetChunkDataEnd(stream, chunk.Size, end); + stream.Position = start; - if ((AniFrameChunkType)chunk.FourCc is AniFrameChunkType.Icon) + while (stream.Position + AniConstants.ChunkHeaderSize <= end) { - int resourceIndex = resources.Count; + AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); + long dataStart = stream.Position; + long dataEnd = GetChunkDataEnd(stream, chunk.Size, end); - // Sequence entries index the physical resource table, so ignored corrupt resources retain an empty slot. - resources.Add(null); - - // Unsequenced resources are consumed in physical order; sequenced files need only the referenced indices. - bool shouldDecode = !hasSequence || sequence.Contains((uint)resourceIndex); - if (shouldDecode) + if ((AniFrameChunkType)chunk.FourCc is AniFrameChunkType.Icon) { - this.ExecuteImageDataSegmentAction(() => + int resourceIndex = resources.Count; + + // Sequence entries index the physical resource table, so ignored corrupt resources retain an empty slot. + resources.Add(null); + + if (hasSequence) { - // Child decoders may seek according to embedded offsets; the bounded view prevents crossing the icon chunk. - frameStream.Reset(dataStart, chunk.Size); - AniFrameFormat format = this.GetFrameFormat(frameStream); + while (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] < (uint)resourceIndex) + { + requiredResourceIndex++; + } + } - // Format probing consumes the directory prefix, while the selected child decoder requires the complete resource. - frameStream.Position = 0; - resources[resourceIndex] = (format, action(format, frameStream)); - }); + // Unsequenced resources are consumed in physical order; sequenced files need only the referenced indices. + bool shouldDecode = !hasSequence + || (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] == (uint)resourceIndex); - if (resources[resourceIndex] is not null) + if (shouldDecode) { - decodedResourceCount++; + this.ExecuteImageDataSegmentAction(() => + { + // Child decoders may seek according to embedded offsets; the bounded view prevents crossing the icon chunk. + frameStream.Reset(dataStart, chunk.Size); + AniFrameFormat format = this.GetFrameFormat(frameStream); + + // Format probing consumes the directory prefix, while the selected child decoder requires the complete resource. + frameStream.Position = 0; + resources[resourceIndex] = (format, action(format, frameStream)); + }); + + if (resources[resourceIndex] is not null) + { + decodedResourceCount++; + } } - } - // Every decoded resource contributes at least one output frame, while a sequence cannot reference later indices. - if ((!hasSequence && decodedResourceCount == maxDecodedResources) - || (hasSequence && (uint)resourceIndex == lastRequiredResource)) - { - return; + // Every decoded resource contributes at least one output frame, while a sequence cannot reference later indices. + if ((!hasSequence && decodedResourceCount == maxDecodedResources) + || (hasSequence && (uint)resourceIndex == lastRequiredResource)) + { + return; + } } - } - stream.Position = GetPaddedEnd(dataEnd, chunk.Size, end); + stream.Position = GetPaddedEnd(dataEnd, chunk.Size, end); + } } } + finally + { + sortedSequenceOwner?.Dispose(); + } } /// diff --git a/src/ImageSharp/Formats/Ani/AniEncoderCore.cs b/src/ImageSharp/Formats/Ani/AniEncoderCore.cs index 2b42d8cb55..96b5fb5751 100644 --- a/src/ImageSharp/Formats/Ani/AniEncoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniEncoderCore.cs @@ -54,11 +54,22 @@ public void Encode(Image image, Stream stream, CancellationToken AniFrameMetadata firstMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); AniFrameFormat firstFormat = firstMetadata.FrameFormat; bool bitmapResources = firstFormat is AniFrameFormat.Bmp; + bool writeSequence = imageMetadata.Flags.HasFlag(AniHeaderFlags.ContainsSequence); uint displayRate = firstMetadata.FrameDelay is 0 ? imageMetadata.DisplayRate : firstMetadata.FrameDelay; bool hasVariableRates = false; int groupCount = 0; int maxGroupSize = 1; + if (bitmapResources && imageMetadata.BitCount is not (0 or 1 or 2 or 4 or 8 or 16 or 24 or 32)) + { + throw new ImageFormatException("ANI bitmap resources require a supported bit depth."); + } + + if (bitmapResources && imageMetadata.Planes is not (0 or 1)) + { + throw new ImageFormatException("ANI bitmap resources require exactly one color plane."); + } + // This validation pass derives the fixed ANI header and largest icon directory without allocating a grouping graph. // Encoding repeats the linear grouping scan below, trading a cheap pass for zero per-group collections. for (int frameIndex = 0; frameIndex < image.Frames.Count;) @@ -66,6 +77,12 @@ public void Encode(Image image, Stream stream, CancellationToken AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); int groupSize = 1; + if (metadata.FrameFormat is not (AniFrameFormat.Ico or AniFrameFormat.Cur or AniFrameFormat.Bmp)) + { + // FrameFormat is public metadata and therefore must be validated before any container bytes are written. + throw new ImageFormatException("ANI contains an unsupported embedded frame format."); + } + // Positive sequence numbers group adjacent resolution variants; non-positive values form independent steps. if (metadata.SequenceNumber > 0) { @@ -118,9 +135,9 @@ public void Encode(Image image, Stream stream, CancellationToken Width = bitmapResources ? imageMetadata.Width is 0 ? (uint)image.Width : imageMetadata.Width : 0, Height = bitmapResources ? imageMetadata.Height is 0 ? (uint)image.Height : imageMetadata.Height : 0, BitCount = bitmapResources ? imageMetadata.BitCount is 0 ? 32U : imageMetadata.BitCount : 0, - Planes = bitmapResources ? imageMetadata.Planes is 0 ? 1U : imageMetadata.Planes : 0, + Planes = bitmapResources ? 1U : 0, DisplayRate = displayRate, - Flags = bitmapResources ? 0 : AniHeaderFlags.IsIcon + Flags = (bitmapResources ? 0 : AniHeaderFlags.IsIcon) | (writeSequence ? AniHeaderFlags.ContainsSequence : 0) }; // One allocator-owned directory buffer is sliced and reused for every icon resource; its capacity is the largest group. @@ -131,6 +148,11 @@ public void Encode(Image image, Stream stream, CancellationToken long riffSizePosition = this.BeginContainer(stream, AniConstants.RiffFourCc, AniConstants.AniFormTypeFourCc); this.WriteHeader(stream, header); + if (writeSequence) + { + this.WriteSequence(stream, groupCount); + } + if (hasVariableRates) { this.WriteRates(stream, image, displayRate); @@ -182,6 +204,27 @@ private void WriteHeader(Stream stream, AniHeader header) this.EndChunk(stream, sizePosition); } + /// + /// Writes an identity sequence table when the source metadata declares an explicit sequence. + /// + /// The destination stream. + /// The number of animation steps. + private void WriteSequence(Stream stream, int stepCount) + { + long sizePosition = this.BeginChunk(stream, "seq "u8); + Span value = this.buffer[..sizeof(uint)]; + + // Decoding expands source resource references into presentation order. Encoding writes those expanded steps as + // distinct resources, so an identity table preserves the explicit-sequence flag without changing playback. + for (uint i = 0; i < stepCount; i++) + { + BinaryPrimitives.WriteUInt32LittleEndian(value, i); + stream.Write(value); + } + + this.EndChunk(stream, sizePosition); + } + /// /// Writes per-step rates when they cannot be represented by one header value. /// @@ -291,6 +334,7 @@ private void WriteFrameResource(Image image, Stream stream, int { PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, Quantizer = this.encoder.Quantizer, + SkipMetadata = this.encoder.SkipMetadata, TransparentColorMode = this.encoder.TransparentColorMode }); @@ -302,6 +346,7 @@ private void WriteFrameResource(Image image, Stream stream, int { PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, Quantizer = this.encoder.Quantizer, + SkipMetadata = this.encoder.SkipMetadata, TransparentColorMode = this.encoder.TransparentColorMode }); @@ -318,6 +363,7 @@ private void WriteFrameResource(Image image, Stream stream, int PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, Quantizer = this.encoder.Quantizer, SkipFileHeader = true, + SkipMetadata = this.encoder.SkipMetadata, SupportTransparency = bitCount is 32, TransparentColorMode = this.encoder.TransparentColorMode }; diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index c66dc17410..b72d7c1e3f 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -60,11 +60,11 @@ private AniMetadata(AniMetadata other) public uint BitCount { get; set; } /// - /// Gets or sets the color-plane count declared by the ANI header. + /// Gets or sets the number of independently addressable color planes declared by the ANI header. /// /// - /// Bitmap-based ANI files use one plane. Icon-based files commonly store zero because this header field is reserved - /// when the embedded resources are ICO or CUR data. + /// Bitmap-based ANI files use the Windows DIB plane value, which must be one. Icon-based ANI files use zero because + /// each embedded ICO or CUR entry describes its own pixel layout. No other values are defined by the format. /// public uint Planes { get; set; } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index f543e4f3dc..111f61fbfb 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -102,6 +102,11 @@ internal sealed class BmpEncoderCore /// private readonly bool skipFileHeader; + /// + /// Whether optional image metadata should be omitted. + /// + private readonly bool skipMetadata; + /// private readonly bool isDoubleHeight; @@ -122,6 +127,7 @@ public BmpEncoderCore(BmpEncoder encoder, MemoryAllocator memoryAllocator) this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; this.processedAlphaMask = encoder.ProcessedAlphaMask; this.skipFileHeader = encoder.SkipFileHeader; + this.skipMetadata = encoder.SkipMetadata; this.isDoubleHeight = encoder.UseDoubleHeight; } @@ -174,7 +180,7 @@ internal void Encode(ImageFrame frame, ImageMetadata metadata, S byte[]? iccProfileData = null; int iccProfileSize = 0; - if (metadata.IccProfile != null) + if (!this.skipMetadata && metadata.IccProfile != null) { this.infoHeaderType = BmpInfoHeaderType.WinVersion5; iccProfileData = metadata.IccProfile.ToByteArray(); @@ -229,7 +235,8 @@ private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderS int hResolution = 0; int vResolution = 0; - if (metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio + if (!this.skipMetadata + && metadata.ResolutionUnits != PixelResolutionUnit.AspectRatio && metadata.HorizontalResolution > 0 && metadata.VerticalResolution > 0) { diff --git a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs index c1c6ee3080..79d7e37869 100644 --- a/src/ImageSharp/Formats/Icon/IconDecoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconDecoderCore.cs @@ -78,7 +78,8 @@ protected override Image Decode(BufferedReadStream stream, Cance throw new InvalidImageContentException("The icon file does not contain any decodable image entries."); } - ImageMetadata metadata = new(); + // General profiles belong to the icon result even though the first successfully decoded child image is temporary. + ImageMetadata metadata = decodedEntries[0].Image.Metadata.DeepClone(); BmpMetadata? bmpMetadata = null; PngMetadata? pngMetadata = null; ImageFrame[] frames = new ImageFrame[decodedCount]; @@ -93,7 +94,7 @@ protected override Image Decode(BufferedReadStream stream, Cance Image decoded = decodedEntries[i].Image; ref IconDirEntry entry = ref this.entries[decodedEntries[i].EntryIndex]; ImageFrame source = decoded.Frames.RootFrameUnsafe; - ImageFrame target = new(this.Options.Configuration, this.Dimensions); + ImageFrame target = new(this.Options.Configuration, this.Dimensions, source.Metadata.DeepClone()); frames[i] = target; initializedFrameCount++; @@ -109,8 +110,6 @@ protected override Image Decode(BufferedReadStream stream, Cance { pngMetadata = decoded.Metadata.GetPngMetadata(); } - - target.Metadata.SetFormatMetadata(PngFormat.Instance, source.Metadata.GetPngMetadata()); } else { @@ -194,7 +193,21 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes); ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, frameStream, cancellationToken); - ImageFrameMetadata frameMetadata = new(); + ImageFrameMetadata frameMetadata = frameInfo.FrameMetadataCollection.Count is 0 ? new ImageFrameMetadata() : frameInfo.FrameMetadataCollection[0].DeepClone(); + + if (frameCount is 0) + { + // The container has one image-level metadata object, so the first valid entry supplies general profiles and resolution. + ImageMetadata sourceMetadata = frameInfo.Metadata; + metadata.HorizontalResolution = sourceMetadata.HorizontalResolution; + metadata.VerticalResolution = sourceMetadata.VerticalResolution; + metadata.ResolutionUnits = sourceMetadata.ResolutionUnits; + metadata.ExifProfile = sourceMetadata.ExifProfile?.DeepClone(); + metadata.IccProfile = sourceMetadata.IccProfile?.DeepClone(); + metadata.IptcProfile = sourceMetadata.IptcProfile?.DeepClone(); + metadata.XmpProfile = sourceMetadata.XmpProfile?.DeepClone(); + metadata.CicpProfile = sourceMetadata.CicpProfile?.DeepClone(); + } if (isPng) { @@ -202,8 +215,6 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok { pngMetadata = frameInfo.Metadata.GetPngMetadata(); } - - frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata()); } else { diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 97bf96ea2a..8d043d7279 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -116,62 +117,90 @@ internal void Encode(Image image, Stream stream, int height = frame.Height; } - long imageStart = stream.Position; - entries[i].Entry.ImageOffset = checked((uint)(imageStart - basePosition)); - - // ANI flattens variants onto a common canvas, while an icon entry can encode a smaller rectangle. - // Child encoders consume Image, so an isolated cropped image is required to prevent encoding the padded canvas. - using Image encodingFrame = new(image.Configuration, width, height); - for (int y = 0; y < height; y++) + if (width > frame.Width || height > frame.Height) { - frame.PixelBuffer.DangerousGetRowSpan(y)[..width].CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); + // EncodingWidth and EncodingHeight are public metadata, so reject a crop that exceeds the source frame here. + throw new ImageFormatException("The icon encoding dimensions exceed the source frame dimensions."); } + long imageStart = stream.Position; + entries[i].Entry.ImageOffset = checked((uint)(imageStart - basePosition)); ref EncodingFrameMetadata encodingMetadata = ref entries[i]; + Image? encodingImage = null; - // Compression and bitmap depth are per-entry, so the concrete encoder configuration must be selected per frame. - switch (encodingMetadata.Compression) + try { - case IconFrameCompression.Bmp: + bool requiresCrop = width != frame.Width || height != frame.Height; + bool requiresIsolatedImage = encodingMetadata.Compression is IconFrameCompression.Png && image.Frames.Count > 1; + + if (requiresCrop || requiresIsolatedImage) { - BmpEncoder bmpEncoder = new() + // PNG accepts Image rather than ImageFrame, and ANI variants may occupy only part of their common canvas. + // Allocate only for those cases; full-sized BMP frames can be encoded directly from their existing storage. + ImageMetadata? metadata = this.encoder.SkipMetadata || encodingMetadata.Compression is not IconFrameCompression.Png ? null : image.Metadata.DeepClone(); + encodingImage = new Image(image.Configuration, width, height, metadata); + + for (int y = 0; y < height; y++) { - Quantizer = this.GetQuantizer(encodingMetadata, colorTable), - ProcessedAlphaMask = true, - UseDoubleHeight = true, - SkipFileHeader = true, - SupportTransparency = false, - TransparentColorMode = this.encoder.TransparentColorMode, - PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, - BitsPerPixel = encodingMetadata.BmpBitsPerPixel, - SkipMetadata = this.encoder.SkipMetadata - }; - - BmpEncoderCore bmpEncoderCore = new(bmpEncoder, image.Configuration.MemoryAllocator); - bmpEncoderCore.Encode(encodingFrame, stream, cancellationToken); - break; + frame.PixelBuffer.DangerousGetRowSpan(y)[..width].CopyTo(encodingImage.GetRootFramePixelBuffer().DangerousGetRowSpan(y)); + } + + if (!this.encoder.SkipMetadata && encodingMetadata.Compression is IconFrameCompression.Png) + { + encodingImage.Frames.RootFrame.Metadata.SetFormatMetadata(PngFormat.Instance, frame.Metadata.GetPngMetadata().DeepClone()); + } } - case IconFrameCompression.Png: + ImageFrame sourceFrame = encodingImage?.Frames.RootFrame ?? frame; + + // Compression and bitmap depth are per-entry, so the concrete encoder configuration must be selected per frame. + switch (encodingMetadata.Compression) { - PngEncoder pngEncoder = new() + case IconFrameCompression.Bmp: + { + BmpEncoder bmpEncoder = new() + { + Quantizer = this.GetQuantizer(encodingMetadata, colorTable), + ProcessedAlphaMask = true, + UseDoubleHeight = true, + SkipFileHeader = true, + SupportTransparency = false, + TransparentColorMode = this.encoder.TransparentColorMode, + PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, + BitsPerPixel = encodingMetadata.BmpBitsPerPixel, + SkipMetadata = this.encoder.SkipMetadata + }; + + BmpEncoderCore bmpEncoderCore = new(bmpEncoder, image.Configuration.MemoryAllocator); + bmpEncoderCore.Encode(sourceFrame, image.Metadata, stream, cancellationToken); + break; + } + + case IconFrameCompression.Png: { - // Only 32bit Png supported. - // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 - BitDepth = PngBitDepth.Bit8, - ColorType = PngColorType.RgbWithAlpha, - TransparentColorMode = this.encoder.TransparentColorMode, - CompressionLevel = PngCompressionLevel.BestCompression, - SkipMetadata = this.encoder.SkipMetadata - }; - - using PngEncoderCore pngEncoderCore = new(image.Configuration, pngEncoder); - pngEncoderCore.Encode(encodingFrame, stream, cancellationToken); - break; + PngEncoder pngEncoder = new() + { + // Only 32bit Png supported. + // https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473 + BitDepth = PngBitDepth.Bit8, + ColorType = PngColorType.RgbWithAlpha, + TransparentColorMode = this.encoder.TransparentColorMode, + CompressionLevel = PngCompressionLevel.BestCompression, + SkipMetadata = this.encoder.SkipMetadata + }; + + using PngEncoderCore pngEncoderCore = new(image.Configuration, pngEncoder); + pngEncoderCore.Encode(encodingImage ?? image, stream, cancellationToken); + break; + } + + default: + throw new NotSupportedException(); } - - default: - throw new NotSupportedException(); + } + finally + { + encodingImage?.Dispose(); } encodingMetadata.Entry.BytesInRes = checked((uint)(stream.Position - imageStart)); diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs index bc5e4ebb82..e1ff17fec0 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Ani; -[Trait("format", "Ani")] +[Trait("Format", "Ani")] [ValidateDisposedMemoryAllocations] public class AniDecoderTests { diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs index 8f905a093e..8b0246c504 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs @@ -3,8 +3,11 @@ using System.Buffers.Binary; using SixLabors.ImageSharp.Formats.Ani; +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestDataIcc; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Ani; @@ -133,6 +136,47 @@ public void AniEncoder_RoundTripsOtherFrameFormats(AniFrameFormat frameFormat) Assert.Equal(frameFormat, decoded.Frames.RootFrame.Metadata.GetAniMetadata().FrameFormat); } + /// + /// Verifies that metadata suppression is propagated to every embedded resource encoder. + /// + [Theory] + [InlineData(AniFrameFormat.Ico)] + [InlineData(AniFrameFormat.Cur)] + [InlineData(AniFrameFormat.Bmp)] + public void AniEncoder_SkipMetadataPropagatesToEmbeddedEncoder(AniFrameFormat frameFormat) + { + using Image image = new(16, 16, Color.Red.ToPixel()); + image.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.ProfileRandomArray); + + AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); + imageMetadata.BitCount = 32; + imageMetadata.Planes = 1; + + AniFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); + frameMetadata.FrameFormat = frameFormat; + frameMetadata.Compression = IconFrameCompression.Bmp; + frameMetadata.BmpBitsPerPixel = BmpBitsPerPixel.Bit32; + + using MemoryStream stream = new(); + image.Save(stream, new AniEncoder { SkipMetadata = true }); + + ReadOnlySpan encoded = stream.GetBuffer().AsSpan(0, (int)stream.Length); + int frameChunkOffset = encoded.IndexOf("icon"u8); + Assert.True(frameChunkOffset >= 0); + + ReadOnlySpan resource = encoded[(frameChunkOffset + AniConstants.ChunkHeaderSize)..]; + int dibOffset = 0; + + if (frameFormat is not AniFrameFormat.Bmp) + { + dibOffset = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(resource[(IconDir.Size + IconDirEntry.Size - sizeof(uint))..])); + } + + // Metadata-free ICO/CUR bitmaps use BITMAPINFOHEADER, while raw transparent ANI bitmaps require BITMAPV4HEADER. + int expectedHeaderSize = frameFormat is AniFrameFormat.Bmp ? BmpInfoHeader.SizeV4 : BmpInfoHeader.SizeV3; + Assert.Equal(expectedHeaderSize, BinaryPrimitives.ReadInt32LittleEndian(resource[dibOffset..])); + } + /// /// Verifies that an independent frame cannot collide with an explicit sequence group. /// @@ -153,4 +197,46 @@ public void AniEncoder_NonPositiveSequenceDoesNotCollideWithExplicitGroup() Assert.Equal(1, decoded.Frames[0].Metadata.GetAniMetadata().SequenceNumber); Assert.Equal(2, decoded.Frames[1].Metadata.GetAniMetadata().SequenceNumber); } + + /// + /// Verifies that an explicit source sequence is preserved as an identity table after playback-order expansion. + /// + [Fact] + public void AniEncoder_PreservesExplicitSequence() + { + using Image image = new(16, 16, Color.Red.ToPixel()); + image.Frames.AddFrame(image.Frames.RootFrame); + image.Metadata.GetAniMetadata().Flags = AniHeaderFlags.IsIcon | AniHeaderFlags.ContainsSequence; + + using MemoryStream stream = new(); + image.Save(stream, new AniEncoder()); + + ReadOnlySpan data = stream.GetBuffer().AsSpan(0, (int)stream.Length); + int sequenceOffset = data.IndexOf("seq "u8); + + Assert.True(sequenceOffset >= 0); + Assert.Equal((uint)(2 * sizeof(uint)), BinaryPrimitives.ReadUInt32LittleEndian(data[(sequenceOffset + sizeof(uint))..])); + Assert.Equal(0U, BinaryPrimitives.ReadUInt32LittleEndian(data[(sequenceOffset + AniConstants.ChunkHeaderSize)..])); + Assert.Equal(1U, BinaryPrimitives.ReadUInt32LittleEndian(data[(sequenceOffset + AniConstants.ChunkHeaderSize + sizeof(uint))..])); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + + Assert.True(decoded.Metadata.GetAniMetadata().Flags.HasFlag(AniHeaderFlags.ContainsSequence)); + } + + /// + /// Verifies that unsupported public frame metadata is rejected before any container data is written. + /// + [Fact] + public void AniEncoder_UnsupportedFrameFormatThrowsBeforeWriting() + { + using Image image = new(16, 16); + image.Frames.RootFrame.Metadata.GetAniMetadata().FrameFormat = (AniFrameFormat)byte.MaxValue; + + using MemoryStream stream = new(); + + Assert.Throws(() => image.Save(stream, new AniEncoder())); + Assert.Equal(0, stream.Length); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index 4c7438d568..f637d64bf3 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -4,6 +4,8 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Formats.Icon; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Cur; @@ -121,4 +123,22 @@ public void Encode_WithTransparentColorBehaviorClear_Works() } }); } + + [Fact] + public void PngEntry_PreservesExifProfile() + { + using Image image = new(16, 16); + image.Metadata.ExifProfile = new ExifProfile(); + image.Metadata.ExifProfile.SetValue(ExifTag.Software, "ImageSharp"); + image.Frames.RootFrame.Metadata.GetIcoMetadata().Compression = IconFrameCompression.Png; + + using MemoryStream stream = new(); + image.Save(stream, Encoder); + + stream.Position = 0; + using Image decoded = Image.Load(stream); + + Assert.NotNull(decoded.Metadata.ExifProfile); + Assert.Equal(image.Metadata.ExifProfile.Values, decoded.Metadata.ExifProfile.Values); + } } From eadb2a6c79bc219049fa443357f9a18a4c51e8e5 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 27 Jul 2026 13:40:55 +1000 Subject: [PATCH 20/20] Handle oversized ANI chunks and partial icon masks --- src/ImageSharp/Formats/Ani/AniConstants.cs | 9 +++ src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 11 +++- src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs | 6 +- .../Formats/Ani/AniDecoderTests.cs | 62 +++++++++++++++++++ .../Formats/Icon/Ico/IcoEncoderTests.cs | 31 ++++++++++ 5 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs index 16925cc657..773fe7c267 100644 --- a/src/ImageSharp/Formats/Ani/AniConstants.cs +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -23,6 +23,15 @@ internal static class AniConstants /// public const int IconDirHeaderSize = 6; + /// + /// The maximum number of bytes retained from an ancillary chunk. + /// + /// + /// Control arrays and information strings come from untrusted input. Bounding them independently of the allocator + /// prevents a physically large RIFF chunk from consuming an unreasonable amount of memory. + /// + public const int MaxAncillaryChunkSize = 8 * 1024 * 1024; + /// /// The list of MIME types that identify ANI data. /// diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index 39583cfe4b..cdfcf30164 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -411,6 +411,14 @@ private void ReadUInt32Values(BufferedReadStream stream, uint chunkSize, string return; } + // MaxFrames controls retained animation steps, but its default is intentionally unbounded. Apply a separate + // byte limit before allocation so an oversized control chunk follows ancillary integrity handling. + if (chunkSize > AniConstants.MaxAncillaryChunkSize) + { + this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk is too large."); + return; + } + int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames); if (count is 0) { @@ -494,7 +502,8 @@ private bool TryReadText(BufferedReadStream stream, uint chunkSize, ref IMemoryO { value = null; - if (chunkSize > int.MaxValue) + // INFO text is optional metadata. Reject or skip oversized values before renting their backing buffer. + if (chunkSize > AniConstants.MaxAncillaryChunkSize) { this.ThrowOrIgnoreNonStrictSegmentError("The ANI information text chunk is too large."); return false; diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index 111f61fbfb..af6582420e 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -890,7 +890,7 @@ private static void ProcessedAlphaMask(Stream stream, ImageFrame where TPixel : unmanaged, IPixel { // Each byte represents eight pixels and every scanline is padded to a 4-byte DIB boundary. - int arrayWidth = encodingFrame.Width / 8; + int arrayWidth = (encodingFrame.Width + 7) / 8; int padding = arrayWidth % 4; if (padding is not 0) { @@ -910,7 +910,9 @@ private static void ProcessedAlphaMask(Stream stream, ImageFrame { int x = i * 8; - for (int j = 0; j < 8; j++) + // The final byte can represent fewer than eight pixels when the image width is not byte-aligned. + int pixelCount = Math.Min(8, encodingFrame.Width - x); + for (int j = 0; j < pixelCount; j++) { WriteAlphaMask(row[x + j], ref mask[i], j); } diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs index e1ff17fec0..93de4d2d9f 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -145,4 +145,66 @@ public void AniDecoder_InvalidRateChunk_FollowsIntegrityHandling() DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); } + + /// + /// Verifies that oversized control arrays are rejected before allocation and follow ancillary integrity handling. + /// + /// to append a sequence chunk; otherwise, a rate chunk. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AniDecoder_OversizedControlChunk_FollowsIntegrityHandling(bool sequence) + { + byte[] source = TestFile.Create(Help).Bytes.ToArray(); + int chunkOffset = (source.Length + 1) & ~1; + int payloadSize = AniConstants.MaxAncillaryChunkSize + sizeof(uint); + byte[] data = new byte[chunkOffset + AniConstants.ChunkHeaderSize + payloadSize]; + source.CopyTo(data, 0); + + ReadOnlySpan identifier = sequence ? "seq "u8 : "rate"u8; + identifier.CopyTo(data.AsSpan(chunkOffset)); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(chunkOffset + sizeof(uint)), (uint)payloadSize); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - AniConstants.ChunkHeaderSize); + + using MemoryStream defaultStream = new(data, false); + using Image image = AniDecoder.Instance.Decode(DecoderOptions.Default, defaultStream); + + Assert.Equal(4, image.Frames.Count); + + using MemoryStream strictStream = new(data, false); + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); + } + + /// + /// Verifies that oversized information text is rejected before allocation and follows ancillary integrity handling. + /// + [Fact] + public void AniDecoder_OversizedInformationText_FollowsIntegrityHandling() + { + byte[] source = TestFile.Create(Help).Bytes.ToArray(); + int listOffset = (source.Length + 1) & ~1; + int textSize = AniConstants.MaxAncillaryChunkSize + 1; + int paddedTextSize = textSize + (textSize & 1); + int listSize = sizeof(uint) + AniConstants.ChunkHeaderSize + paddedTextSize; + byte[] data = new byte[listOffset + AniConstants.ChunkHeaderSize + listSize]; + source.CopyTo(data, 0); + + "LIST"u8.CopyTo(data.AsSpan(listOffset)); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(listOffset + sizeof(uint)), (uint)listSize); + "INFO"u8.CopyTo(data.AsSpan(listOffset + AniConstants.ChunkHeaderSize)); + int textOffset = listOffset + AniConstants.ChunkHeaderSize + sizeof(uint); + "INAM"u8.CopyTo(data.AsSpan(textOffset)); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(textOffset + sizeof(uint)), (uint)textSize); + BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - AniConstants.ChunkHeaderSize); + + using MemoryStream defaultStream = new(data, false); + using Image image = AniDecoder.Instance.Decode(DecoderOptions.Default, defaultStream); + + Assert.Equal(4, image.Frames.Count); + + using MemoryStream strictStream = new(data, false); + DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Assert.Throws(() => AniDecoder.Instance.Decode(strict, strictStream)); + } } diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs index f637d64bf3..08bb537ad7 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoEncoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Icon; @@ -141,4 +142,34 @@ public void PngEntry_PreservesExifProfile() Assert.NotNull(decoded.Metadata.ExifProfile); Assert.Equal(image.Metadata.ExifProfile.Values, decoded.Metadata.ExifProfile.Values); } + + /// + /// Verifies that the final partial AND-mask byte contains every pixel when the bitmap width is not byte-aligned. + /// + /// The bitmap width to encode. + [Theory] + [InlineData(1)] + [InlineData(7)] + [InlineData(9)] + [InlineData(15)] + public void BmpEntry_WritesPartialAlphaMaskByte(int width) + { + using Image image = new(width, 1, Color.Red.ToPixel()); + image[width - 1, 0] = Color.Transparent.ToPixel(); + + IcoFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetIcoMetadata(); + metadata.Compression = IconFrameCompression.Bmp; + metadata.BmpBitsPerPixel = BmpBitsPerPixel.Bit32; + + using MemoryStream stream = new(); + image.Save(stream, Encoder); + + // These widths produce one DWORD-aligned mask row at the end of the bitmap resource. + ReadOnlySpan mask = stream.GetBuffer().AsSpan(checked((int)stream.Length) - sizeof(uint), sizeof(uint)); + int pixelIndex = width - 1; + int byteIndex = pixelIndex / 8; + int bitIndex = pixelIndex % 8; + + Assert.Equal((byte)(0b10000000 >> bitIndex), mask[byteIndex]); + } }