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.Qoi; 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