From aac7d3ab8a788e35b2cc6d468673ff23e0162f59 Mon Sep 17 00:00:00 2001 From: Betta_Fish Date: Sat, 1 Feb 2025 02:31:36 +0800 Subject: [PATCH 01/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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.