From e58010abea2ce8c0290954c86f8a62c5553a29a8 Mon Sep 17 00:00:00 2001 From: Poker Date: Wed, 5 Mar 2025 15:49:52 +0800 Subject: [PATCH] finish decoder --- src/ImageSharp/Formats/Ani/AniChunk.cs | 38 -- src/ImageSharp/Formats/Ani/AniChunkType.cs | 65 ++- .../Formats/Ani/AniConfigurationModule.cs | 18 + src/ImageSharp/Formats/Ani/AniConstants.cs | 25 +- src/ImageSharp/Formats/Ani/AniDecoderCore.cs | 427 +++++++++++++----- src/ImageSharp/Formats/Ani/AniFormat.cs | 19 +- .../Formats/Ani/AniFrameMetadata.cs | 70 +++ src/ImageSharp/Formats/Ani/AniHeader.cs | 18 +- .../Formats/Ani/AniImageFormatDetector.cs | 30 ++ src/ImageSharp/Formats/Ani/AniMetadata.cs | 47 +- .../Formats/Bmp/BmpRenderingIntent.cs | 2 +- src/ImageSharp/Formats/Icon/IconDir.cs | 16 +- src/ImageSharp/Formats/Icon/IconDirEntry.cs | 9 +- .../Formats/Icon/IconEncoderCore.cs | 20 +- src/ImageSharp/Formats/Icon/IconFileType.cs | 2 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 6 +- .../Formats/Webp/Chunks/WebpVp8X.cs | 2 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 2 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 2 +- src/ImageSharp/Formats/Webp/RiffHelper.cs | 154 +++---- src/ImageSharp/Formats/Webp/WebpConstants.cs | 29 +- .../Formats/Webp/WebpImageFormatDetector.cs | 24 +- .../_Generated/ImageMetadataExtensions.cs | 20 + src/ImageSharp/IO/BufferedReadStream.cs | 57 ++- .../Formats/Ani/AniDecoderTests.cs | 7 +- tests/ImageSharp.Tests/TestImages.cs | 3 +- tests/Images/Input/Ani/Help.ani | 3 + tests/Images/Input/Ani/aero_busy.ani | 3 + 28 files changed, 784 insertions(+), 334 deletions(-) delete mode 100644 src/ImageSharp/Formats/Ani/AniChunk.cs create mode 100644 src/ImageSharp/Formats/Ani/AniConfigurationModule.cs create mode 100644 src/ImageSharp/Formats/Ani/AniFrameMetadata.cs create mode 100644 src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs create mode 100644 tests/Images/Input/Ani/Help.ani create mode 100644 tests/Images/Input/Ani/aero_busy.ani diff --git a/src/ImageSharp/Formats/Ani/AniChunk.cs b/src/ImageSharp/Formats/Ani/AniChunk.cs deleted file mode 100644 index d644749223..0000000000 --- a/src/ImageSharp/Formats/Ani/AniChunk.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -#nullable disable -using System.Buffers; -using SixLabors.ImageSharp.Formats.Png; - -namespace SixLabors.ImageSharp.Formats.Ani; - -internal readonly struct AniChunk -{ - public AniChunk(int length, AniChunkType type, IMemoryOwner data = null) - { - this.Length = length; - this.Type = type; - this.Data = data; - } - - /// - /// Gets the length. - /// An unsigned integer giving the number of bytes in the chunk's - /// data field. The length counts only the data field, not itself, - /// the chunk type code, or the CRC. Zero is a valid length - /// - public int Length { get; } - - /// - /// Gets the chunk type. - /// The value is the equal to the UInt32BigEndian encoding of its 4 ASCII characters. - /// - public AniChunkType Type { get; } - - /// - /// Gets the data bytes appropriate to the chunk type, if any. - /// This field can be of zero length or null. - /// - public IMemoryOwner Data { get; } -} diff --git a/src/ImageSharp/Formats/Ani/AniChunkType.cs b/src/ImageSharp/Formats/Ani/AniChunkType.cs index 00419cae5b..35eba01140 100644 --- a/src/ImageSharp/Formats/Ani/AniChunkType.cs +++ b/src/ImageSharp/Formats/Ani/AniChunkType.cs @@ -5,7 +5,66 @@ namespace SixLabors.ImageSharp.Formats.Ani; internal enum AniChunkType : uint { - Seq = 0x73_65_71_20, - Rate = 0x72_61_74_65, - List = 0x4C_49_53_54 + /// + /// "anih" + /// + AniH = 0x68_69_6E_61, + + /// + /// "seq " + /// + Seq = 0x20_71_65_73, + + /// + /// "rate" + /// + Rate = 0x65_74_61_72, + + /// + /// "LIST" + /// + List = 0x54_53_49_4C +} + +/// +/// ListType +/// +internal enum AniListType : uint +{ + /// + /// "INFO" (ListType) + /// + Info = 0x4F_46_4E_49, + + /// + /// "fram" + /// + Fram = 0x6D_61_72_66 +} + +/// +/// in "INFO" +/// +internal enum AniListInfoType : uint +{ + /// + /// "INAM" + /// + INam = 0x4D_41_4E_49, + + /// + /// "IART" + /// + IArt = 0x54_52_41_49 +} + +/// +/// in "Fram" +/// +internal enum AniListFrameType : uint +{ + /// + /// "icon" + /// + Icon = 0x6E_6F_63_69 } diff --git a/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs new file mode 100644 index 0000000000..6cf3a55f10 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniConfigurationModule.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Registers the image encoders, decoders and mime type detectors for the Ico format. +/// +public sealed class AniConfigurationModule : IImageFormatConfigurationModule +{ + /// + public void Configure(Configuration configuration) + { + // configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder()); + configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector()); + } +} diff --git a/src/ImageSharp/Formats/Ani/AniConstants.cs b/src/ImageSharp/Formats/Ani/AniConstants.cs index 7d59098c40..0b40efa31f 100644 --- a/src/ImageSharp/Formats/Ani/AniConstants.cs +++ b/src/ImageSharp/Formats/Ani/AniConstants.cs @@ -8,14 +8,31 @@ internal static class AniConstants /// /// The list of mime types that equate to an ani. /// - /// - /// See - /// - public static readonly IEnumerable MimeTypes = []; + public static readonly IEnumerable MimeTypes = ["application/x-navi-animation"]; /// /// The list of file extensions that equate to an ani. /// public static readonly IEnumerable FileExtensions = ["ani"]; + /// + /// Gets the header bytes identifying an ani. + /// + public static ReadOnlySpan AniFormTypeFourCc => "ACON"u8; + + /// + /// Gets the header bytes identifying an ani. + /// + public const uint AniFourCc = 0x41_43_4F_4E; + + public static class ChunkFourCcs + { + public static ReadOnlySpan AniHeader => "anih"u8; + + public static ReadOnlySpan Seq => "seq "u8; + + public static ReadOnlySpan Rate => "rate"u8; + + public static ReadOnlySpan Icon => "icon"u8; + } } diff --git a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs index f6b0c98a1c..ab79a14e0a 100644 --- a/src/ImageSharp/Formats/Ani/AniDecoderCore.cs +++ b/src/ImageSharp/Formats/Ani/AniDecoderCore.cs @@ -2,23 +2,36 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.Buffers.Binary; +using System.Collections; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Icon; -using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; -internal class AniDecoderCore : ImageDecoderCore +internal class AniDecoderCore(DecoderOptions options) : ImageDecoderCore(options) { + private enum ListIconChunkType + { + Ico = 1, + Cur = 2, + Bmp = 3 + } + /// /// The general decoder options. /// - private readonly Configuration configuration; + private readonly Configuration configuration = options.Configuration; /// /// The stream to decode from. @@ -27,157 +40,354 @@ internal class AniDecoderCore : ImageDecoderCore private AniHeader header; - public AniDecoderCore(DecoderOptions options) - : base(options) => this.configuration = options.Configuration; - protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.currentStream = stream; - this.ReadHeader(); + + Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); + long dataSize = riffHeader.Size; + long dataStartPosition = this.currentStream.Position; + ImageMetadata metadata = new(); - AniMetadata aniMetadata = metadata.GetAniMetadata(); - Image? image = null; + AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + + List<(ListIconChunkType Type, Image Image)> frames = []; + this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk); - Span buffer = stackalloc byte[20]; + List> list = []; - try + foreach (int i in sequence) { - while (this.TryReadChunk(buffer, out AniChunk chunk)) + (ListIconChunkType type, Image? img) = frames[i]; + byte? encodingWidth = null; + byte? encodingHeight = null; + bool isRootFrame = true; + list.AddRange(img.Frames.Select(source => { - try + ImageFrame target = new(this.Options.Configuration, this.Dimensions); + for (int y = 0; y < source.Height; y++) { - switch (chunk.Type) - { - case AniChunkType.Seq: + source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); + } - break; - case AniChunkType.Rate: + switch (type) + { + case ListIconChunkType.Ico: + IcoFrameMetadata icoFrameMetadata = source.Metadata.GetIcoMetadata(); + target.Metadata.SetFormatMetadata(IcoFormat.Instance, icoFrameMetadata); + if (isRootFrame) + { + encodingWidth ??= icoFrameMetadata.EncodingWidth; + encodingHeight ??= icoFrameMetadata.EncodingHeight; + } + + break; + case ListIconChunkType.Cur: + CurFrameMetadata curFrameMetadata = source.Metadata.GetCurMetadata(); + target.Metadata.SetFormatMetadata(CurFormat.Instance, curFrameMetadata); + if (isRootFrame) + { + encodingWidth ??= curFrameMetadata.EncodingWidth; + encodingHeight ??= curFrameMetadata.EncodingHeight; + } + + break; + case ListIconChunkType.Bmp: + if (isRootFrame) + { + encodingWidth = Narrow(source.Width); + encodingHeight = Narrow(source.Height); + } + + break; + default: + break; + } - break; - case AniChunkType.List: + isRootFrame = false; - break; - default: - break; + return target; + })); + + ImageFrameMetadata rootFrameMetadata = img.Frames.RootFrame.Metadata; + AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); + aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameCount = img.Frames.Count; + aniFrameMetadata.EncodingWidth = encodingWidth; + aniFrameMetadata.EncodingHeight = encodingHeight; + aniFrameMetadata.SubImageMetadata = img.Metadata; + aniMetadata.IconFrames.Add(rootFrameMetadata); + } + + foreach ((ListIconChunkType _, Image img) in frames) + { + img.Dispose(); + } + + Image image = new(this.Options.Configuration, metadata, list); + + return image; + + void DecodeFrameChunk() + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) + { + continue; + } + + long endPosition = this.currentStream.Position + chunk.Size; + Image? frame = null; + ListIconChunkType type = default; + if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) + { + if (this.currentStream.TryReadUnmanaged(out IconDir dir)) + { + this.currentStream.Position -= Unsafe.SizeOf(); + + switch (dir.Type) + { + case IconFileType.CUR: + frame = CurDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Cur; + break; + case IconFileType.ICO: + frame = IcoDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Ico; + break; + } } } - finally + else { - chunk.Data?.Dispose(); + frame = BmpDecoder.Instance.Decode(this.Options, this.currentStream); + type = ListIconChunkType.Bmp; } - } - } - catch - { - image?.Dispose(); - throw; - } + if (frame is not null) + { + frames.Add((type, frame)); + this.Dimensions = new(Math.Max(this.Dimensions.Width, frame.Size.Width), Math.Max(this.Dimensions.Height, frame.Size.Height)); + } - throw new NotImplementedException(); + this.currentStream.Position = endPosition; + } + } } - private void ReadSeq() + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { + this.currentStream = stream; - } + Guard.IsTrue(this.currentStream.TryReadUnmanaged(out RiffOrListChunkHeader riffHeader), nameof(riffHeader), "Invalid RIFF header."); + long dataSize = riffHeader.Size; + long dataStartPosition = this.currentStream.Position; - private bool TryReadChunk(Span buffer, out AniChunk chunk) - { - if (!this.TryReadChunkLength(buffer, out int length)) - { - // IEND - chunk = default; - return false; - } + ImageMetadata metadata = new(); + AniMetadata aniMetadata = this.ReadHeader(dataStartPosition, dataSize, metadata); + + List<(ListIconChunkType Type, ImageInfo Info)> infoList = []; + this.HandleRiffChunk(out Span sequence, out Span rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk); + + ImageInfo imageInfo = new(this.Dimensions, metadata, (IReadOnlyList)aniMetadata.IconFrames); - while (length < 0) + foreach (int i in sequence) { - // Not a valid chunk so try again until we reach a known chunk. - if (!this.TryReadChunkLength(buffer, out length)) + (ListIconChunkType type, ImageInfo info) = infoList[i]; + + ImageFrameMetadata rootFrameMetadata = imageInfo.FrameMetadataCollection is [var first, ..] ? first : new(); + AniFrameMetadata aniFrameMetadata = rootFrameMetadata.GetAniMetadata(); + aniFrameMetadata.Rate = rate == default ? aniMetadata.DisplayRate : rate[i]; + aniFrameMetadata.FrameCount = info.FrameMetadataCollection.Count; + aniFrameMetadata.EncodingWidth = type switch { - // IEND - chunk = default; - return false; - } + ListIconChunkType.Bmp => Narrow(info.Width), + ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingWidth, + ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingWidth, + _ => null + }; + aniFrameMetadata.EncodingHeight = type switch + { + ListIconChunkType.Bmp => Narrow(info.Height), + ListIconChunkType.Cur => rootFrameMetadata.GetCurMetadata().EncodingHeight, + ListIconChunkType.Ico => rootFrameMetadata.GetIcoMetadata().EncodingHeight, + _ => null + }; + aniFrameMetadata.SubImageMetadata = info.Metadata; + aniMetadata.IconFrames.Add(rootFrameMetadata); } - AniChunkType type = this.ReadChunkType(buffer); + return imageInfo; - // A chunk might report a length that exceeds the length of the stream. - // Take the minimum of the two values to ensure we don't read past the end of the stream. - long position = this.currentStream.Position; - chunk = new AniChunk( - length: (int)Math.Min(length, this.currentStream.Length - position), - type: type, - data: this.ReadChunkData(length)); + void IdentifyFrameChunk() + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + if ((AniListFrameType)chunk.FourCc is not AniListFrameType.Icon) + { + continue; + } - return true; - } + long endPosition = this.currentStream.Position + chunk.Size; + ImageInfo? info = null; + ListIconChunkType type = default; + if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon)) + { + if (this.currentStream.TryReadUnmanaged(out IconDir dir)) + { + this.currentStream.Position -= Unsafe.SizeOf(); + + switch (dir.Type) + { + case IconFileType.CUR: + info = CurDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Cur; + break; + case IconFileType.ICO: + info = IcoDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Ico; + break; + } + } + } + else + { + info = BmpDecoder.Instance.Identify(this.Options, this.currentStream); + type = ListIconChunkType.Bmp; + } + if (info is not null) + { + infoList.Add((type, info)); + this.Dimensions = new(Math.Max(this.Dimensions.Width, info.Size.Width), Math.Max(this.Dimensions.Height, info.Size.Height)); + } - protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) - { - throw new NotImplementedException(); + this.currentStream.Position = endPosition; + } + } } - private void ReadHeader() + private AniMetadata ReadHeader(long dataStartPosition, long dataSize, ImageMetadata metadata) { - // Skip the identifier - this.currentStream.Skip(12); - Span buffer = stackalloc byte[36]; - _ = this.currentStream.Read(buffer); - this.header = AniHeader.Parse(buffer); - } + if (!this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader riffChunkHeader) || + (AniChunkType)riffChunkHeader.FourCc is not AniChunkType.AniH) + { + Guard.IsTrue(false, nameof(riffChunkHeader), "Missing ANIH chunk."); + } - private void ReadSeq(Stream stream) - { - Span buffer = stackalloc byte[4]; - int length = BinaryPrimitives.ReadInt32BigEndian(buffer); + AniMetadata aniMetadata = metadata.GetAniMetadata(); + + if (this.currentStream.TryReadUnmanaged(out AniHeader result)) + { + this.header = result; + aniMetadata.Width = result.Width; + aniMetadata.Height = result.Height; + aniMetadata.BitCount = result.BitCount; + aniMetadata.Planes = result.Planes; + aniMetadata.DisplayRate = result.DisplayRate; + aniMetadata.Flags = result.Flags; + } + return aniMetadata; } /// - /// Attempts to read the length of the next chunk. + /// Call
+ /// -> Call
+ /// -> Call ///
- /// Temporary buffer. - /// The result length. If the return type is this parameter is passed uninitialized. - /// - /// Whether the length was read. - /// - [MethodImpl(InliningOptions.ShortMethod)] - private bool TryReadChunkLength(Span buffer, out int result) + private void HandleRiffChunk(out Span sequence, out Span rate, long dataStartPosition, long dataSize, AniMetadata aniMetadata, ICollection totalFrameCount, Action handleFrameChunk) { - if (this.currentStream.Read(buffer, 0, 4) == 4) + sequence = default; + rate = default; + + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) { - result = BinaryPrimitives.ReadInt32BigEndian(buffer); + switch ((AniChunkType)chunk.FourCc) + { + case AniChunkType.Seq: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + sequence = MemoryMarshal.Cast(data.Memory.Span); + break; + } - return true; + case AniChunkType.Rate: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + rate = MemoryMarshal.Cast(data.Memory.Span); + break; + } + + case AniChunkType.List: + this.HandleListChunk(dataStartPosition, dataSize, aniMetadata, handleFrameChunk); + break; + default: + break; + } } - result = 0; - return false; + if (sequence == default) + { + sequence = Enumerable.Range(0, totalFrameCount.Count).ToArray(); + } } - /// - /// Identifies the chunk type from the chunk. - /// - /// Temporary buffer. - /// - /// Thrown if the input stream is not valid. - /// - [MethodImpl(InliningOptions.ShortMethod)] - private AniChunkType ReadChunkType(Span buffer) + private void HandleListChunk(long dataStartPosition, long dataSize, AniMetadata aniMetadata, Action handleFrameChunk) { - if (this.currentStream.Read(buffer, 0, 4) == 4) + if (!this.currentStream.TryReadUnmanaged(out uint listType)) + { + return; + } + + switch ((AniListType)listType) { - return (AniChunkType)BinaryPrimitives.ReadUInt32BigEndian(buffer); + case AniListType.Fram: + { + handleFrameChunk(); + break; + } + + case AniListType.Info: + { + while (this.TryReadChunk(dataStartPosition, dataSize, out RiffChunkHeader chunk)) + { + switch ((AniListInfoType)chunk.FourCc) + { + case AniListInfoType.INam: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + aniMetadata.Name = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); + break; + } + + case AniListInfoType.IArt: + { + using IMemoryOwner data = this.ReadChunkData(chunk.Size); + aniMetadata.Artist = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0'); + break; + } + + default: + break; + } + } + + break; + } } + } - PngThrowHelper.ThrowInvalidChunkType(); + private bool TryReadChunk(long startPosition, long size, out RiffChunkHeader chunk) + { + if (this.currentStream.Position - startPosition >= size) + { + chunk = default; + return false; + } - // The IDE cannot detect the throw here. - return default; + return this.currentStream.TryReadUnmanaged(out chunk); } /// @@ -185,9 +395,9 @@ internal class AniDecoderCore : ImageDecoderCore /// /// The length of the chunk data to read. [MethodImpl(InliningOptions.ShortMethod)] - private IMemoryOwner ReadChunkData(int length) + private IMemoryOwner ReadChunkData(uint length) { - if (length == 0) + if (length is 0) { return new BasicArrayBuffer([]); } @@ -195,12 +405,13 @@ internal class AniDecoderCore : ImageDecoderCore // We rent the buffer here to return it afterwards in Decode() // We don't want to throw a degenerated memory exception here as we want to allow partial decoding // so limit the length. - length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); - IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); + int len = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position); + IMemoryOwner buffer = this.configuration.MemoryAllocator.Allocate(len, AllocationOptions.Clean); - this.currentStream.Read(buffer.GetSpan(), 0, length); + this.currentStream.Read(buffer.GetSpan(), 0, len); return buffer; } + private static byte Narrow(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; } diff --git a/src/ImageSharp/Formats/Ani/AniFormat.cs b/src/ImageSharp/Formats/Ani/AniFormat.cs index 16f707390a..cc13e1aae8 100644 --- a/src/ImageSharp/Formats/Ani/AniFormat.cs +++ b/src/ImageSharp/Formats/Ani/AniFormat.cs @@ -1,16 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Ico; - namespace SixLabors.ImageSharp.Formats.Ani; - /// /// Registers the image encoders, decoders and mime type detectors for the bmp format. /// -public sealed class AniFormat : IImageFormat +public sealed class AniFormat : IImageFormat { /// /// Gets the shared instance. @@ -18,17 +14,20 @@ public sealed class AniFormat : IImageFormat public static AniFormat Instance { get; } = new(); /// - public AniMetadata CreateDefaultFormatMetadata() => throw new NotImplementedException(); + public string Name => "ANI"; /// - public string Name => "ANI"; + public string DefaultMimeType => "application/x-navi-animation"; + + /// + public IEnumerable MimeTypes => AniConstants.MimeTypes; /// - public string DefaultMimeType { get; } + public IEnumerable FileExtensions => AniConstants.FileExtensions; /// - public IEnumerable MimeTypes { get; } + public AniMetadata CreateDefaultFormatMetadata() => new(); /// - public IEnumerable FileExtensions { get; } + public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new(); } diff --git a/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs new file mode 100644 index 0000000000..8b2b0b9175 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniFrameMetadata.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Provides Ani specific metadata information for the image. +/// +public class AniFrameMetadata : IFormatFrameMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public AniFrameMetadata() + { + } + + /// + /// Gets or sets the display time for this frame (in 1/60 seconds) + /// + public uint Rate { get; set; } + + /// + /// Gets or sets the frames count of **one** "icon" chunk. + /// + public int FrameCount { get; set; } = 1; + + /// + /// Gets or sets the encoding width.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingWidth { get; set; } + + /// + /// Gets or sets the encoding height.
+ /// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater. + ///
+ public byte? EncodingHeight { get; set; } + + /// + /// Gets or sets the of one "icon" chunk. + /// + public ImageMetadata? SubImageMetadata { get; set; } + + /// + public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) => + new() + { + Rate = (uint)metadata.Duration.TotalSeconds * 60 + }; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => new AniFrameMetadata { Rate = this.Rate }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() => new FormatConnectingFrameMetadata() { Duration = TimeSpan.FromSeconds(this.Rate / 60d) }; + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination) + where TPixel : unmanaged, IPixel + { + throw new NotImplementedException(); + } + + /// + public AniFrameMetadata DeepClone() => new() { Rate = this.Rate }; +} diff --git a/src/ImageSharp/Formats/Ani/AniHeader.cs b/src/ImageSharp/Formats/Ani/AniHeader.cs index 514ce710d3..8f504c5118 100644 --- a/src/ImageSharp/Formats/Ani/AniHeader.cs +++ b/src/ImageSharp/Formats/Ani/AniHeader.cs @@ -1,11 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Ani; -[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 36)] internal readonly struct AniHeader { public uint Size { get; } @@ -26,16 +26,24 @@ internal readonly struct AniHeader public AniHeaderFlags Flags { get; } - public static AniHeader Parse(in ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref AniHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(in Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public void WriteTo(in Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } +/// +/// Flags for the ANI header. +/// [Flags] public enum AniHeaderFlags : uint { + /// + /// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file. + /// IsIcon = 1, + + /// + /// If set, the ANI file contains a "seq " chunk. + /// ContainsSeq = 2 } diff --git a/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs new file mode 100644 index 0000000000..83826da111 --- /dev/null +++ b/src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Webp; + +namespace SixLabors.ImageSharp.Formats.Ani; + +/// +/// Detects ico file headers. +/// +public class AniImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => RiffOrListChunkHeader.HeaderSize; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? AniFormat.Instance : null; + return format is not null; + } + + private bool IsSupportedFileFormat(ReadOnlySpan header) + => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is + { + IsRiff: true, + FormType: AniConstants.AniFourCc + }; +} diff --git a/src/ImageSharp/Formats/Ani/AniMetadata.cs b/src/ImageSharp/Formats/Ani/AniMetadata.cs index d373a7d9a5..72d2b83272 100644 --- a/src/ImageSharp/Formats/Ani/AniMetadata.cs +++ b/src/ImageSharp/Formats/Ani/AniMetadata.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Ico; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Ani; @@ -11,6 +11,51 @@ namespace SixLabors.ImageSharp.Formats.Ani; ///
public class AniMetadata : IFormatMetadata { + /// + /// Gets or sets the width of frames in the animation. + /// + public uint Width { get; set; } + + /// + /// Gets or sets the height of frames in the animation. + /// + public uint Height { get; set; } + + /// + /// Gets or sets the number of bits per pixel. + /// + public uint BitCount { get; set; } + + /// + /// Gets or sets the number of frames in the animation. + /// + public uint Planes { get; set; } + + /// + /// Gets or sets the default display rate of frames in the animation. + /// + public uint DisplayRate { get; set; } + + /// + /// Gets or sets the flags for the ANI header. + /// + public AniHeaderFlags Flags { get; set; } + + /// + /// Gets or sets the name of the ANI file. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the artist of the ANI file. + /// + public string? Artist { get; set; } + + /// + /// Gets or sets the each "icon" chunk in ANI file. + /// + public IList IconFrames { get; set; } = []; + /// /// Initializes a new instance of the class. /// diff --git a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs index 32dbb4a51e..87a1f19cc7 100644 --- a/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs +++ b/src/ImageSharp/Formats/Bmp/BmpRenderingIntent.cs @@ -17,7 +17,7 @@ internal enum BmpRenderingIntent /// /// Maintains saturation. Used for business charts and other situations in which undithered colors are required. /// - LCS_GM_BUSINESS = 1, + LCS_GM_BUSINESS = 1, /// /// Maintains colorimetric match. Used for graphic designs and named colors. diff --git a/src/ImageSharp/Formats/Icon/IconDir.cs b/src/ImageSharp/Formats/Icon/IconDir.cs index 3e02538c84..28681e6ea3 100644 --- a/src/ImageSharp/Formats/Icon/IconDir.cs +++ b/src/ImageSharp/Formats/Icon/IconDir.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; @@ -25,19 +26,14 @@ internal struct IconDir(ushort reserved, IconFileType type, ushort count) /// public ushort Count = count; - public IconDir(IconFileType type) - : this(type, 0) - { - } - - public IconDir(IconFileType type, ushort count) + public IconDir(IconFileType type, ushort count = 0) : this(0, type, count) { } - public static IconDir Parse(ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref IconDir Parse(ReadOnlySpan data) + => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public readonly void WriteTo(Stream stream) + => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } diff --git a/src/ImageSharp/Formats/Icon/IconDirEntry.cs b/src/ImageSharp/Formats/Icon/IconDirEntry.cs index eab15dd872..598ec47b6e 100644 --- a/src/ImageSharp/Formats/Icon/IconDirEntry.cs +++ b/src/ImageSharp/Formats/Icon/IconDirEntry.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Icon; @@ -52,9 +53,9 @@ internal struct IconDirEntry ///
public uint ImageOffset; - public static IconDirEntry Parse(in ReadOnlySpan data) - => MemoryMarshal.Cast(data)[0]; + public static ref IconDirEntry Parse(in ReadOnlySpan data) + => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); - public readonly unsafe void WriteTo(in Stream stream) - => stream.Write(MemoryMarshal.Cast([this])); + public readonly void WriteTo(in Stream stream) + => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); } diff --git a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs index 03e01f912f..3965a05328 100644 --- a/src/ImageSharp/Formats/Icon/IconEncoderCore.cs +++ b/src/ImageSharp/Formats/Icon/IconEncoderCore.cs @@ -120,17 +120,17 @@ internal abstract class IconEncoderCore 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 @@ internal abstract class BitWriterBase 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 @@ internal abstract class BitWriterBase 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 @@ internal abstract class BitWriterBase 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 @@ internal readonly struct WebpVp8X : IEquatable 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 @@ internal class Vp8LEncoder : IDisposable 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 @@ internal class Vp8Encoder : IDisposable if (hasAnimation) { - RiffHelper.EndWriteChunk(stream, prevPosition); + RiffHelper.EndWriteChunk(stream, prevPosition, 2); } } finally diff --git a/src/ImageSharp/Formats/Webp/RiffHelper.cs b/src/ImageSharp/Formats/Webp/RiffHelper.cs index b6318c7486..e40c398f94 100644 --- a/src/ImageSharp/Formats/Webp/RiffHelper.cs +++ b/src/ImageSharp/Formats/Webp/RiffHelper.cs @@ -2,138 +2,122 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; -using System.Text; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.Chunks; namespace SixLabors.ImageSharp.Formats.Webp; internal static class RiffHelper { - /// - /// The header bytes identifying RIFF file. - /// - private const uint RiffFourCc = 0x52_49_46_46; + public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + { + long pos = BeginWriteChunk(stream, fourCc); + stream.Write(data); + EndWriteChunk(stream, pos); + } - public static void WriteRiffFile(Stream stream, string formType, Action func) => - WriteChunk(stream, RiffFourCc, s => - { - s.Write(Encoding.ASCII.GetBytes(formType)); - func(s); - }); + public static void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) + where TStruct : unmanaged => + WriteChunk(stream, fourCc, MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in chunk, 1))); - public static void WriteChunk(Stream stream, uint fourCc, Action func) + public static long BeginWriteChunk(Stream stream, ReadOnlySpan fourCc) { - Span buffer = stackalloc byte[4]; - // write the fourCC - BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); + stream.Write(fourCc); long sizePosition = stream.Position; - stream.Position += 4; - - func(stream); - - long position = stream.Position; - - uint dataSize = (uint)(position - sizePosition - 4); - // padding - if (dataSize % 2 == 1) - { - stream.WriteByte(0); - position++; - } + // Leaving the place for the size + stream.Position += 4; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); - stream.Position = sizePosition; - stream.Write(buffer); - stream.Position = position; + return sizePosition; } - public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan data) + public static long BeginWriteChunk(Stream stream, uint fourCc) { Span buffer = stackalloc byte[4]; - - // write the fourCC BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); - uint size = (uint)data.Length; - BinaryPrimitives.WriteUInt32LittleEndian(buffer, size); - stream.Write(buffer); - stream.Write(data); - - // padding - if (size % 2 is 1) - { - stream.WriteByte(0); - } + return BeginWriteChunk(stream, buffer); } - public static unsafe void WriteChunk(Stream stream, uint fourCc, in TStruct chunk) - where TStruct : unmanaged + public static long BeginWriteRiff(Stream stream, ReadOnlySpan formType) { - fixed (TStruct* ptr = &chunk) - { - WriteChunk(stream, fourCc, new Span(ptr, sizeof(TStruct))); - } + long sizePosition = BeginWriteChunk(stream, "RIFF"u8); + stream.Write(formType); + return sizePosition; } - public static long BeginWriteChunk(Stream stream, uint fourCc) + public static long BeginWriteList(Stream stream, ReadOnlySpan listType) { - Span buffer = stackalloc byte[4]; - - // write the fourCC - BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc); - stream.Write(buffer); - - long sizePosition = stream.Position; - stream.Position += 4; - + long sizePosition = BeginWriteChunk(stream, "LIST"u8); + stream.Write(listType); return sizePosition; } - public static void EndWriteChunk(Stream stream, long sizePosition) + public static void EndWriteChunk(Stream stream, long sizePosition, int alignment = 1) { - Span buffer = stackalloc byte[4]; + Guard.MustBeGreaterThan(alignment, 0, nameof(alignment)); - long position = stream.Position; + long currentPosition = stream.Position; - uint dataSize = (uint)(position - sizePosition - 4); + uint dataSize = (uint)(currentPosition - sizePosition - 4); - // padding - if (dataSize % 2 is 1) + // Add padding + while (dataSize % alignment is not 0) { stream.WriteByte(0); - position++; + dataSize++; + currentPosition++; } // Add the size of the encoded file to the Riff header. - BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize); stream.Position = sizePosition; - stream.Write(buffer); - stream.Position = position; + stream.Write(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref dataSize), sizeof(uint))); + stream.Position = currentPosition; } - public static long BeginWriteRiffFile(Stream stream, string formType) + public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X, long initPosition) { - long sizePosition = BeginWriteChunk(stream, RiffFourCc); - stream.Write(Encoding.ASCII.GetBytes(formType)); - return sizePosition; - } - - public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition) - { - EndWriteChunk(stream, sizePosition + 4); + // Jump through "RIFF" fourCC + EndWriteChunk(stream, initPosition + 4, 2); // Write the VP8X chunk if necessary. - if (updateVp8x) + if (updateVp8X) { long position = stream.Position; - stream.Position = sizePosition + 12; - vp8x.WriteTo(stream); + stream.Position = initPosition + 12; + vp8X.WriteTo(stream); stream.Position = position; } } } + +internal readonly struct RiffChunkHeader +{ + public readonly uint FourCc; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); + + public readonly uint Size; +} + +internal readonly struct RiffOrListChunkHeader +{ + public const int HeaderSize = 12; + + public readonly uint FourCc; + + public readonly uint Size; + + public readonly uint FormType; + + public ReadOnlySpan FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint)); + + public bool IsRiff => this.FourCc is 0x52_49_46_46; // "RIFF" + + public bool IsList => this.FourCc is 0x4C_49_53_54; // "LIST" + + public static ref RiffOrListChunkHeader Parse(ReadOnlySpan data) => ref Unsafe.As(ref MemoryMarshal.GetReference(data)); +} diff --git a/src/ImageSharp/Formats/Webp/WebpConstants.cs b/src/ImageSharp/Formats/Webp/WebpConstants.cs index 818c843ea9..56b989794b 100644 --- a/src/ImageSharp/Formats/Webp/WebpConstants.cs +++ b/src/ImageSharp/Formats/Webp/WebpConstants.cs @@ -29,36 +29,19 @@ internal static class WebpConstants }; /// - /// Signature byte which identifies a VP8L header. + /// Gets the header bytes identifying a Webp. /// - public const byte Vp8LHeaderMagicByte = 0x2F; + public static ReadOnlySpan WebpFormTypeFourCc => "WEBP"u8; /// - /// The header bytes identifying RIFF file. + /// Gets the header bytes identifying a Webp. /// - public static readonly byte[] RiffFourCc = - { - 0x52, // R - 0x49, // I - 0x46, // F - 0x46 // F - }; + public const uint WebpFourCc = 0x57_45_42_50; /// - /// The header bytes identifying a Webp. - /// - public static readonly byte[] WebpHeader = - { - 0x57, // W - 0x45, // E - 0x42, // B - 0x50 // P - }; - - /// - /// The header bytes identifying a Webp. + /// Signature byte which identifies a VP8L header. /// - public const string WebpFourCc = "WEBP"; + public const byte Vp8LHeaderMagicByte = 0x2F; /// /// 3 bits reserved for version. diff --git a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs index 2b91aa95fe..a7f8d43672 100644 --- a/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs @@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; public sealed class WebpImageFormatDetector : IImageFormatDetector { /// - public int HeaderSize => 12; + public int HeaderSize => RiffOrListChunkHeader.HeaderSize; /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) @@ -21,21 +21,9 @@ public sealed class WebpImageFormatDetector : IImageFormatDetector } private bool IsSupportedFileFormat(ReadOnlySpan header) - => header.Length >= this.HeaderSize && IsRiffContainer(header) && IsWebpFile(header); - - /// - /// Checks, if the header starts with a valid RIFF FourCC. - /// - /// The header bytes. - /// True, if its a valid RIFF FourCC. - private static bool IsRiffContainer(ReadOnlySpan header) - => header[..4].SequenceEqual(WebpConstants.RiffFourCc); - - /// - /// Checks if 'WEBP' is present in the header. - /// - /// The header bytes. - /// True, if its a webp file. - private static bool IsWebpFile(ReadOnlySpan header) - => header.Slice(8, 4).SequenceEqual(WebpConstants.WebpHeader); + => header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is + { + IsRiff: true, + FormType: WebpConstants.WebpFourCc + }; } diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index d3542de7af..2dc9e81589 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -304,6 +304,26 @@ public static class ImageMetadataExtensions /// The new public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image frame metadata. + /// + /// The + /// + public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image frame metadata. + /// The new + public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance); + /// /// Gets the from .
/// If none is found, an instance is created either by conversion from the decoded image format metadata diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 1aa53d65e1..e1e5597dec 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -3,6 +3,12 @@ using System.Buffers; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Ani; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Runtime.InteropServices; +using System; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.IO; @@ -22,10 +28,14 @@ internal sealed class BufferedReadStream : Stream private readonly unsafe byte* pinnedReadBuffer; - // Index within our buffer, not reader position. + /// + /// Index within our buffer, not reader position. + /// private int readBufferIndex; - // Matches what the stream position would be without buffering + /// + /// Matches what the stream position would be without buffering + /// private long readerPosition; private bool isDisposed; @@ -194,6 +204,49 @@ internal sealed class BufferedReadStream : Stream return this.ReadToBufferViaCopyFast(buffer); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryReadUnmanaged(out T result) + where T : unmanaged + { + this.cancellationToken.ThrowIfCancellationRequested(); + + int size = Unsafe.SizeOf(); + + if (size > this.BufferSize) + { + Span span = stackalloc byte[size]; + if (this.ReadToBufferDirectSlow(span) != size) + { + result = default; + return false; + } + + result = MemoryMarshal.Read(span); + } + else + { + if ((uint)this.readBufferIndex > (uint)(this.BufferSize - size)) + { + this.FillReadBuffer(); + } + + if (this.GetCopyCount(size) != size) + { + this.EofHitCount++; + result = default; + return false; + } + + Span span = this.readBuffer.AsSpan(this.readBufferIndex, size); + + this.readerPosition += size; + this.readBufferIndex += size; + result = MemoryMarshal.Read(span); + } + + return true; + } + /// public override void Flush() { diff --git a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs index f79570f22c..17439e3092 100644 --- a/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs @@ -2,9 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Ani; -using SixLabors.ImageSharp.Formats.Bmp; -using SixLabors.ImageSharp.Formats.Cur; -using SixLabors.ImageSharp.Formats.Icon; using SixLabors.ImageSharp.PixelFormats; using static SixLabors.ImageSharp.Tests.TestImages.Ani; @@ -16,7 +13,9 @@ public class AniDecoderTests { [Theory] [WithFile(Work, PixelTypes.Rgba32)] - public void CurDecoder_Decode(TestImageProvider provider) + [WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)] + [WithFile(Help, PixelTypes.Rgba32)] + public void AniDecoder_Decode(TestImageProvider provider) { using Image image = provider.GetImage(AniDecoder.Instance); } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2284167c80..ed43cffcd2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1260,6 +1260,7 @@ public static class TestImages public static class Ani { public const string Work = "Ani/Work.ani"; + public const string MultiFramesInEveryIconChunk = "Ani/aero_busy.ani"; + public const string Help = "Ani/Help.ani"; } - } diff --git a/tests/Images/Input/Ani/Help.ani b/tests/Images/Input/Ani/Help.ani new file mode 100644 index 0000000000..d623503cac --- /dev/null +++ b/tests/Images/Input/Ani/Help.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c49cbb1ca0a3f268695a80df93b1ce2b2cba335a80e8244dd3a702863159bd99 +size 12998 diff --git a/tests/Images/Input/Ani/aero_busy.ani b/tests/Images/Input/Ani/aero_busy.ani new file mode 100644 index 0000000000..e2fa9d31a5 --- /dev/null +++ b/tests/Images/Input/Ani/aero_busy.ani @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff38afb523490e1a9f157c0447bc616b19c22df88bdb45c163243d834e9745f8 +size 556304