diff --git a/src/ImageSharp/Formats/Ani/AniChunkType.cs b/src/ImageSharp/Formats/Ani/AniChunkType.cs
new file mode 100644
index 0000000000..35eba01140
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniChunkType.cs
@@ -0,0 +1,70 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+namespace SixLabors.ImageSharp.Formats.Ani;
+
+internal enum AniChunkType : uint
+{
+ ///
+ /// "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
new file mode 100644
index 0000000000..b5b282de55
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniConstants.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+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.
+ ///
+ 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;
+}
diff --git a/src/ImageSharp/Formats/Ani/AniDecoder.cs b/src/ImageSharp/Formats/Ani/AniDecoder.cs
new file mode 100644
index 0000000000..5cc06ae157
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniDecoder.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Formats.Ani;
+
+///
+/// Decoder for generating an image out of an ani encoded stream.
+///
+public 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..57f9d4031f
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs
@@ -0,0 +1,430 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+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.Webp;
+using SixLabors.ImageSharp.IO;
+using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Memory.Internals;
+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) =>
+ this.configuration = options.Configuration;
+
+ protected override Image Decode(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;
+
+ ImageMetadata metadata = new();
+ AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata);
+
+ List<(AniFrameFormat Type, Image Image)> frames = [];
+ this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk);
+
+ List> list = [];
+
+ for (int i = 0; i < sequence.Length; i++)
+ {
+ int sequenceIndex = sequence[i];
+ (AniFrameFormat 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);
+ for (int y = 0; y < source.Height; y++)
+ {
+ source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y));
+ }
+
+ AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone();
+ source.Metadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata);
+ clonedMetadata.FrameFormat = type;
+ switch (type)
+ {
+ case AniFrameFormat.Ico:
+ IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata();
+ // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null);
+ clonedMetadata.IcoFrameMetadata = icoFrameMetadata;
+ clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
+ clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
+ break;
+ case AniFrameFormat.Cur:
+ CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata();
+ // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null);
+ clonedMetadata.CurFrameMetadata = curFrameMetadata;
+ clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
+ clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
+ break;
+ case AniFrameFormat.Bmp:
+ clonedMetadata.EncodingWidth = Narrow(source.Width);
+ clonedMetadata.EncodingHeight = Narrow(source.Height);
+ break;
+ default:
+ break;
+ }
+
+ return target;
+ }));
+ }
+
+ foreach ((AniFrameFormat _, 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;
+ AniFrameFormat 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 = AniFrameFormat.Cur;
+ break;
+ case IconFileType.ICO:
+ frame = IcoDecoder.Instance.Decode(this.Options, this.currentStream);
+ type = AniFrameFormat.Ico;
+ break;
+ }
+ }
+ }
+ else
+ {
+ frame = BmpDecoder.Instance.Decode(this.Options, this.currentStream);
+ type = AniFrameFormat.Bmp;
+ }
+
+ 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));
+ }
+
+ this.currentStream.Position = endPosition;
+ }
+ }
+ }
+
+ 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;
+
+ ImageMetadata metadata = new();
+ AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata);
+
+ List<(AniFrameFormat Type, ImageInfo Info)> infoList = [];
+ this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk);
+
+ List frameMetadataCollection = new(sequence.Length);
+
+ for (int i = 0; i < sequence.Length; i++)
+ {
+ int sequenceIndex = sequence[i];
+ (AniFrameFormat type, ImageInfo info) = infoList[sequenceIndex];
+
+ AniFrameMetadata aniFrameMetadata = new()
+ {
+ FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex],
+ SequenceNumber = i
+ };
+
+ if (info.FrameMetadataCollection.Count is not 0)
+ {
+ frameMetadataCollection.AddRange(
+ info.FrameMetadataCollection.Select(frameMetadata =>
+ {
+ AniFrameMetadata clonedMetadata = aniFrameMetadata.DeepClone();
+ frameMetadata.SetFormatMetadata(AniFormat.Instance, clonedMetadata);
+ clonedMetadata.FrameFormat = type;
+ switch (type)
+ {
+ case AniFrameFormat.Ico:
+ IcoFrameMetadata icoFrameMetadata = frameMetadata.GetIcoMetadata();
+ // TODO source.Metadata.SetFormatMetadata(IcoFormat.Instance, null);
+ clonedMetadata.IcoFrameMetadata = icoFrameMetadata;
+ clonedMetadata.EncodingWidth = icoFrameMetadata.EncodingWidth;
+ clonedMetadata.EncodingHeight = icoFrameMetadata.EncodingHeight;
+ break;
+ case AniFrameFormat.Cur:
+ CurFrameMetadata curFrameMetadata = frameMetadata.GetCurMetadata();
+ // TODO source.Metadata.SetFormatMetadata(CurFormat.Instance, null);
+ clonedMetadata.CurFrameMetadata = curFrameMetadata;
+ clonedMetadata.EncodingWidth = curFrameMetadata.EncodingWidth;
+ clonedMetadata.EncodingHeight = curFrameMetadata.EncodingHeight;
+ break;
+ default:
+ ThrowHelper.ThrowArgumentOutOfRangeException(nameof(type), "FrameMetadata must be ICO or CUR");
+ break;
+ }
+
+ return frameMetadata;
+ }));
+ }
+ else // BMP
+ {
+ aniFrameMetadata.EncodingWidth = Narrow(info.Width);
+ aniFrameMetadata.EncodingHeight = Narrow(info.Height);
+ aniFrameMetadata.FrameFormat = type;
+ ImageFrameMetadata frameMetadata = new();
+ frameMetadata.SetFormatMetadata(AniFormat.Instance, aniFrameMetadata);
+ frameMetadataCollection.Add(frameMetadata);
+ }
+ }
+
+ ImageInfo imageInfo = new(this.Dimensions, metadata, frameMetadataCollection);
+
+ return imageInfo;
+
+ void IdentifyFrameChunk()
+ {
+ while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk))
+ {
+ if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon)
+ {
+ continue;
+ }
+
+ long endPosition = this.currentStream.Position + chunk.Size;
+ ImageInfo? info = null;
+ AniFrameFormat type = default;
+ if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon))
+ {
+ if (this.currentStream.TryReadUnmanaged(out IconDir dir))
+ {
+ this.currentStream.Position -= Unsafe.SizeOf();
+
+ switch (dir.Type)
+ {
+ case IconFileType.CUR:
+ info = CurDecoder.Instance.Identify(this.Options, this.currentStream);
+ type = AniFrameFormat.Cur;
+ break;
+ case IconFileType.ICO:
+ info = IcoDecoder.Instance.Identify(this.Options, this.currentStream);
+ type = AniFrameFormat.Ico;
+ break;
+ }
+ }
+ }
+ else
+ {
+ info = BmpDecoder.Instance.Identify(this.Options, this.currentStream);
+ type = AniFrameFormat.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));
+ }
+
+ this.currentStream.Position = endPosition;
+ }
+ }
+ }
+
+ private AniMetadata ReadHeader(long dataStartPosition, long dataSize, ImageMetadata metadata)
+ {
+ if (!this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader riffChunkHeader) ||
+ (AniChunkType)riffChunkHeader.FourCc is not AniChunkType.AniH)
+ {
+ Guard.IsTrue(false, nameof(riffChunkHeader), "Missing ANIH chunk.");
+ }
+
+ 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;
+ }
+
+ ///
+ /// Call
+ /// -> Call
+ /// -> Call
+ ///
+ private void HandleRiffChunk(out Span sequence, out Span rate, long dataStartPosition, long dataSize, AniMetadata aniMetadata, ICollection totalFrameCount, Action handleFrameChunk)
+ {
+ sequence = default;
+ rate = default;
+
+ while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk))
+ {
+ switch ((AniChunkType)chunk.FourCc)
+ {
+ case AniChunkType.Seq:
+ {
+ using IMemoryOwner data = this.ReadChunkData(chunk.Size);
+ sequence = MemoryMarshal.Cast(data.Memory.Span);
+ break;
+ }
+
+ 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;
+ }
+ }
+
+ if (sequence.IsEmpty)
+ {
+ sequence = Enumerable.Range(0, totalFrameCount.Count).ToArray();
+ }
+ }
+
+ private void HandleListChunk(long dataStartPosition, long dataSize, AniMetadata aniMetadata, Action handleFrameChunk)
+ {
+ if (!this.currentStream.TryReadUnmanaged(out uint listType))
+ {
+ return;
+ }
+
+ switch ((AniListType)listType)
+ {
+ 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;
+ }
+ }
+ }
+
+ private bool TryReadChunk(long startPosition, long size, out RiffChunkHeader chunk)
+ {
+ if (this.currentStream.Position - startPosition >= size)
+ {
+ chunk = default;
+ return false;
+ }
+
+ return this.currentStream.TryReadUnmanaged(out chunk);
+ }
+
+ ///
+ /// Reads the chunk data from the stream.
+ ///
+ /// The length of the chunk data to read.
+ [MethodImpl(InliningOptions.ShortMethod)]
+ private IMemoryOwner ReadChunkData(uint length)
+ {
+ if (length is 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.
+ 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, 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
new file mode 100644
index 0000000000..cc13e1aae8
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniFormat.cs
@@ -0,0 +1,33 @@
+// 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 bmp format.
+///
+public sealed class AniFormat : IImageFormat
+{
+ ///
+ /// Gets the shared instance.
+ ///
+ public static AniFormat Instance { get; } = new();
+
+ ///
+ public string Name => "ANI";
+
+ ///
+ public string DefaultMimeType => "application/x-navi-animation";
+
+ ///
+ public IEnumerable MimeTypes => AniConstants.MimeTypes;
+
+ ///
+ public IEnumerable FileExtensions => AniConstants.FileExtensions;
+
+ ///
+ public AniMetadata CreateDefaultFormatMetadata() => new();
+
+ ///
+ public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new();
+}
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
new file mode 100644
index 0000000000..f024c1c8ad
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Diagnostics.CodeAnalysis;
+using SixLabors.ImageSharp.Formats.Cur;
+using SixLabors.ImageSharp.Formats.Ico;
+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 FrameDelay { get; set; }
+
+ ///
+ /// Gets or sets the sequence number of current frame.
+ ///
+ public int SequenceNumber { 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 a value indicating whether the frame will be encoded as an ICO or CUR or BMP file.
+ ///
+ public AniFrameFormat FrameFormat { 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) =>
+ new()
+ {
+ FrameDelay = (uint)metadata.Duration.TotalSeconds * 60
+ };
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+
+ ///
+ public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) };
+
+ ///
+ public void AfterFrameApply(ImageFrame source, ImageFrame destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
+ ///
+ public AniFrameMetadata DeepClone() => new()
+ {
+ FrameDelay = this.FrameDelay,
+ EncodingHeight = this.EncodingHeight,
+ EncodingWidth = this.EncodingWidth,
+ SequenceNumber = this.SequenceNumber,
+ IsIco = this.IsIco,
+ IcoFrameMetadata = this.IcoFrameMetadata?.DeepClone(),
+ CurFrameMetadata = this.CurFrameMetadata?.DeepClone(),
+
+ // TODO SubImageMetadata
+ };
+}
diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs
new file mode 100644
index 0000000000..1656911cc4
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniHeader.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace SixLabors.ImageSharp.Formats.Ani;
+
+internal readonly struct AniHeader
+{
+ public uint Size { get; }
+
+ public uint Frames { get; }
+
+ public uint Steps { get; }
+
+ public uint Width { get; }
+
+ public uint Height { get; }
+
+ public uint BitCount { get; }
+
+ public uint Planes { get; }
+
+ public uint DisplayRate { get; }
+
+ public AniHeaderFlags Flags { get; }
+
+ public static ref AniHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data));
+
+ public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
+}
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/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
new file mode 100644
index 0000000000..1660b8a765
--- /dev/null
+++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs
@@ -0,0 +1,108 @@
+// 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 AniMetadata : IFormatMetadata
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ 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; }
+
+ ///
+ /// 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; }
+
+ ///
+ public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
+ => throw new NotImplementedException();
+
+ ///
+ public void AfterImageApply(Image destination)
+ where TPixel : unmanaged, IPixel
+ {
+ }
+
+ ///
+ IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
+
+ ///
+ public PixelTypeInfo GetPixelTypeInfo()
+ => throw new NotImplementedException();
+
+ ///
+ public FormatConnectingMetadata ToFormatConnectingMetadata()
+ => 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
+ };
+}
diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs
index 3e02538c84..36d5d8820e 100644
--- a/src/ImageSharp/Formats/Icon/IconDir.cs
+++ b/src/ImageSharp/Formats/Icon/IconDir.cs
@@ -1,29 +1,30 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
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)
: this(type, 0)
@@ -35,9 +36,16 @@ public IconDir(IconFileType type, ushort count)
{
}
- public static IconDir Parse(ReadOnlySpan data)
- => MemoryMarshal.Cast(data)[0];
+ 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));
- 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/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 b6318c7486..1a409eb8e0 100644
--- a/src/ImageSharp/Formats/Webp/RiffHelper.cs
+++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs
@@ -2,137 +2,93 @@
// 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;
- }
-
- public static long BeginWriteRiffFile(Stream stream, string formType)
- {
- long sizePosition = BeginWriteChunk(stream, RiffFourCc);
- stream.Write(Encoding.ASCII.GetBytes(formType));
- return sizePosition;
+ stream.Write(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref dataSize), sizeof(uint)));
+ stream.Position = currentPosition;
}
- public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition)
+ public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X, long initPosition)
{
- 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;
}
}
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 818c843ea9..89092d3c78 100644
--- a/src/ImageSharp/Formats/Webp/WebpConstants.cs
+++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs
@@ -29,36 +29,14 @@ internal static class WebpConstants
};
///
- /// Signature byte which identifies a VP8L header.
+ /// Gets the header bytes identifying a Webp.
///
- public const byte Vp8LHeaderMagicByte = 0x2F;
+ public const uint WebpFourCc = 0x57_45_42_50;
///
- /// The header bytes identifying RIFF file.
- ///
- public static readonly byte[] RiffFourCc =
- {
- 0x52, // R
- 0x49, // I
- 0x46, // F
- 0x46 // F
- };
-
- ///
- /// 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.
@@ -319,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/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 e35d00ed39..2dc9e81589 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
@@ -283,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..36e0bbb557 100644
--- a/src/ImageSharp/IO/BufferedReadStream.cs
+++ b/src/ImageSharp/IO/BufferedReadStream.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.IO;
@@ -22,10 +23,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 +199,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
new file mode 100644
index 0000000000..17439e3092
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Ani;
+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)]
+ [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 fafa1d2429..ed43cffcd2 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -1256,4 +1256,11 @@ 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";
+ 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/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
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