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