Browse Source

Merge 6fcf75c5d9 into ad816ed988

pull/2899/merge
Poker 3 days ago
committed by GitHub
parent
commit
e423343ec1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 70
      src/ImageSharp/Formats/Ani/AniChunkType.cs
  2. 18
      src/ImageSharp/Formats/Ani/AniConfigurationModule.cs
  3. 27
      src/ImageSharp/Formats/Ani/AniConstants.cs
  4. 41
      src/ImageSharp/Formats/Ani/AniDecoder.cs
  5. 437
      src/ImageSharp/Formats/Ani/AniDecoderCore.cs
  6. 33
      src/ImageSharp/Formats/Ani/AniFormat.cs
  7. 25
      src/ImageSharp/Formats/Ani/AniFrameFormat.cs
  8. 97
      src/ImageSharp/Formats/Ani/AniFrameMetadata.cs
  9. 32
      src/ImageSharp/Formats/Ani/AniHeader.cs
  10. 21
      src/ImageSharp/Formats/Ani/AniHeaderFlags.cs
  11. 30
      src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs
  12. 108
      src/ImageSharp/Formats/Ani/AniMetadata.cs
  13. 24
      src/ImageSharp/Formats/Icon/IconDir.cs
  14. 9
      src/ImageSharp/Formats/Icon/IconDirEntry.cs
  15. 2
      src/ImageSharp/Formats/Icon/IconFileType.cs
  16. 6
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  17. 2
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  18. 2
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  19. 2
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  20. 16
      src/ImageSharp/Formats/Webp/RiffChunkHeader.cs
  21. 126
      src/ImageSharp/Formats/Webp/RiffHelper.cs
  22. 26
      src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs
  23. 15
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  24. 24
      src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs
  25. 41
      src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
  26. 52
      src/ImageSharp/IO/BufferedReadStream.cs
  27. 22
      tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
  28. 7
      tests/ImageSharp.Tests/TestImages.cs
  29. 3
      tests/Images/Input/Ani/Help.ani
  30. 3
      tests/Images/Input/Ani/Work.ani
  31. 3
      tests/Images/Input/Ani/aero_busy.ani

70
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
{
/// <summary>
/// "anih"
/// </summary>
AniH = 0x68_69_6E_61,
/// <summary>
/// "seq "
/// </summary>
Seq = 0x20_71_65_73,
/// <summary>
/// "rate"
/// </summary>
Rate = 0x65_74_61_72,
/// <summary>
/// "LIST"
/// </summary>
List = 0x54_53_49_4C
}
/// <summary>
/// ListType
/// </summary>
internal enum AniListType : uint
{
/// <summary>
/// "INFO" (ListType)
/// </summary>
Info = 0x4F_46_4E_49,
/// <summary>
/// "fram"
/// </summary>
Fram = 0x6D_61_72_66
}
/// <summary>
/// in "INFO"
/// </summary>
internal enum AniListInfoType : uint
{
/// <summary>
/// "INAM"
/// </summary>
INam = 0x4D_41_4E_49,
/// <summary>
/// "IART"
/// </summary>
IArt = 0x54_52_41_49
}
/// <summary>
/// in "Fram"
/// </summary>
internal enum AniListFrameType : uint
{
/// <summary>
/// "icon"
/// </summary>
Icon = 0x6E_6F_63_69
}

18
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// </summary>
public sealed class AniConfigurationModule : IImageFormatConfigurationModule
{
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
// configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder());
configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector());
}
}

27
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
{
/// <summary>
/// Gets the header bytes identifying an ani.
/// </summary>
public const uint AniFourCc = 0x41_43_4F_4E;
/// <summary>
/// The list of mime types that equate to an ani.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = ["application/x-navi-animation"];
/// <summary>
/// The list of file extensions that equate to an ani.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["ani"];
/// <summary>
/// Gets the header bytes identifying an ani.
/// </summary>
public static ReadOnlySpan<byte> AniFormTypeFourCc => "ACON"u8;
}

41
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;
/// <summary>
/// Decoder for generating an image out of an ani encoded stream.
/// </summary>
public sealed class AniDecoder : ImageDecoder
{
private AniDecoder()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>
public static AniDecoder Instance { get; } = new();
/// <inheritdoc/>
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
Image<TPixel> image = new AniDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken);
ScaleToTargetSize(options, image);
return image;
}
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
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);
}
}

437
src/ImageSharp/Formats/Ani/AniDecoderCore.cs

@ -0,0 +1,437 @@
// 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
{
/// <summary>
/// The general decoder options.
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream currentStream = null!;
private AniHeader header;
public AniDecoderCore(DecoderOptions options)
: base(options) =>
this.configuration = options.Configuration;
protected override Image<TPixel> Decode<TPixel>(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<TPixel> Image)> frames = [];
this.HandleRiffChunk(out Span<int> sequence, out Span<uint> rate, dataStartPosition, dataSize, aniMetadata, frames, DecodeFrameChunk);
List<ImageFrame<TPixel>> list = [];
for (int i = 0; i < sequence.Length; i++)
{
int sequenceIndex = sequence[i];
(AniFrameFormat type, Image<TPixel>? img) = frames[sequenceIndex];
AniFrameMetadata aniFrameMetadata = new()
{
FrameDelay = rate.IsEmpty ? aniMetadata.DisplayRate : rate[sequenceIndex],
SequenceNumber = i
};
list.AddRange(img.Frames.Select(source =>
{
ImageFrame<TPixel> 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<TPixel> img) in frames)
{
img.Dispose();
}
Image<TPixel> 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<TPixel>? frame = null;
AniFrameFormat type = default;
if (aniMetadata.Flags.HasFlag(AniHeaderFlags.IsIcon))
{
if (this.currentStream.TryReadUnmanaged(out IconDir dir))
{
this.currentStream.Position -= Unsafe.SizeOf<IconDir>();
switch (dir.Type)
{
case IconFileType.CUR:
frame = CurDecoder.Instance.Decode<TPixel>(this.Options, this.currentStream);
type = AniFrameFormat.Cur;
break;
case IconFileType.ICO:
frame = IcoDecoder.Instance.Decode<TPixel>(this.Options, this.currentStream);
type = AniFrameFormat.Ico;
break;
}
}
}
else
{
frame = BmpDecoder.Instance.Decode<TPixel>(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<int> sequence, out Span<uint> rate, dataStartPosition, dataSize, aniMetadata, infoList, IdentifyFrameChunk);
List<ImageFrameMetadata> 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<IconDir>();
switch (dir.Type)
{
// TODO: Use Core decoders.
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
{
// TODO: Use Core decoders.
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;
}
/// <summary>
/// Call <see cref="HandleRiffChunk"/> <br/>
/// -> Call <see cref="HandleListChunk"/> <br/>
/// -> Call <paramref name="handleFrameChunk"/>
/// </summary>
private void HandleRiffChunk(out Span<int> sequence, out Span<uint> 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<byte> data = this.ReadChunkData(chunk.Size);
sequence = MemoryMarshal.Cast<byte, int>(data.Memory.Span);
break;
}
case AniChunkType.Rate:
{
using IMemoryOwner<byte> data = this.ReadChunkData(chunk.Size);
rate = MemoryMarshal.Cast<byte, uint>(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<byte> data = this.ReadChunkData(chunk.Size);
aniMetadata.Name = Encoding.ASCII.GetString(data.Memory.Span).TrimEnd('\0');
break;
}
case AniListInfoType.IArt:
{
using IMemoryOwner<byte> 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);
}
/// <summary>
/// Reads the chunk data from the stream.
/// </summary>
/// <param name="length">The length of the chunk data to read.</param>
[MethodImpl(InliningOptions.ShortMethod)]
private IMemoryOwner<byte> ReadChunkData(uint length)
{
if (length is 0)
{
return new BasicArrayBuffer<byte>([]);
}
// 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<byte> buffer = this.configuration.MemoryAllocator.Allocate<byte>(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;
}

33
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;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the bmp format.
/// </summary>
public sealed class AniFormat : IImageFormat<AniMetadata, AniFrameMetadata>
{
/// <summary>
/// Gets the shared instance.
/// </summary>
public static AniFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "ANI";
/// <inheritdoc/>
public string DefaultMimeType => "application/x-navi-animation";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => AniConstants.MimeTypes;
/// <inheritdoc/>
public IEnumerable<string> FileExtensions => AniConstants.FileExtensions;
/// <inheritdoc/>
public AniMetadata CreateDefaultFormatMetadata() => new();
/// <inheritdoc/>
public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new();
}

25
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;
/// <summary>
/// Specifies the format of the frame data.
/// </summary>
public enum AniFrameFormat
{
/// <summary>
/// The frame data is in ICO format.
/// </summary>
Ico = 1,
/// <summary>
/// The frame data is in CUR format.
/// </summary>
Cur,
/// <summary>
/// The frame data is in BMP format.
/// </summary>
Bmp
}

97
src/ImageSharp/Formats/Ani/AniFrameMetadata.cs

@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides Ani specific metadata information for the image.
/// </summary>
public class AniFrameMetadata : IFormatFrameMetadata<AniFrameMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class.
/// </summary>
public AniFrameMetadata()
{
}
/// <summary>
/// Gets or sets the display time for this frame (in 1/60 seconds)
/// </summary>
public uint FrameDelay { get; set; }
/// <summary>
/// Gets or sets the sequence number of current frame.
/// </summary>
public int SequenceNumber { get; set; } = 1;
/// <summary>
/// Gets or sets the encoding width. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte? EncodingWidth { get; set; }
/// <summary>
/// Gets or sets the encoding height. <br />
/// Can be any number between 0 and 255. Value 0 means a frame height of 256 pixels or greater.
/// </summary>
public byte? EncodingHeight { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the frame will be encoded as an ICO or CUR or BMP file.
/// </summary>
public AniFrameFormat FrameFormat { get; set; }
/// <summary>
/// Gets or sets the <see cref="IcoFrameMetadata"/> of one "icon" chunk.
/// </summary>
public IcoFrameMetadata? IcoFrameMetadata { get; set; }
/// <summary>
/// Gets or sets the <see cref="CurFrameMetadata"/> of one "icon" chunk.
/// </summary>
public CurFrameMetadata? CurFrameMetadata { get; set; }
/// <inheritdoc/>
public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) =>
new()
{
FrameDelay = (uint)metadata.Duration.TotalSeconds * 60
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
{
// TODO: Implement. You need to consider encoding width/height.
return new() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) };
}
/// <inheritdoc/>
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
// TODO: Implement. You need to consider encoding width/height.
}
/// <inheritdoc/>
public AniFrameMetadata DeepClone() => new()
{
FrameDelay = this.FrameDelay,
EncodingHeight = this.EncodingHeight,
EncodingWidth = this.EncodingWidth,
SequenceNumber = this.SequenceNumber,
FrameFormat = this.FrameFormat,
IcoFrameMetadata = this.IcoFrameMetadata?.DeepClone(),
CurFrameMetadata = this.CurFrameMetadata?.DeepClone(),
// TODO SubImageMetadata
};
}

32
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<byte> data) => ref Unsafe.As<byte, AniHeader>(ref MemoryMarshal.GetReference(data));
public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
}

21
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;
/// <summary>
/// Flags for the ANI header.
/// </summary>
[Flags]
public enum AniHeaderFlags : uint
{
/// <summary>
/// If set, the ANI file's "icon" chunk contains an ICO or CUR file, otherwise it contains a BMP file.
/// </summary>
IsIcon = 1,
/// <summary>
/// If set, the ANI file contains a "seq " chunk.
/// </summary>
ContainsSeq = 2
}

30
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;
/// <summary>
/// Detects ico file headers.
/// </summary>
public class AniImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc/>
public int HeaderSize => RiffOrListChunkHeader.HeaderSize;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
{
format = this.IsSupportedFileFormat(header) ? AniFormat.Instance : null;
return format is not null;
}
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is
{
IsRiff: true,
FormType: AniConstants.AniFourCc
};
}

108
src/ImageSharp/Formats/Ani/AniMetadata.cs

@ -0,0 +1,108 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides Ani specific metadata information for the image.
/// </summary>
public class AniMetadata : IFormatMetadata<AniMetadata>
{
/// <summary>
/// Initializes a new instance of the <see cref="AniMetadata"/> class.
/// </summary>
public AniMetadata()
{
}
/// <summary>
/// Gets or sets the width of frames in the animation.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// </remarks>
public uint Width { get; set; }
/// <summary>
/// Gets or sets the height of frames in the animation.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// </remarks>
public uint Height { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// </remarks>
public uint BitCount { get; set; }
/// <summary>
/// Gets or sets the number of frames in the animation.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// </remarks>
public uint Planes { get; set; }
/// <summary>
/// Gets or sets the default display rate of frames in the animation.
/// </summary>
public uint DisplayRate { get; set; }
/// <summary>
/// Gets or sets the flags for the ANI header.
/// </summary>
public AniHeaderFlags Flags { get; set; }
/// <summary>
/// Gets or sets the name of the ANI file.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the artist of the ANI file.
/// </summary>
public string? Artist { get; set; }
/// <inheritdoc/>
public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
=> throw new NotImplementedException();
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
=> throw new NotImplementedException();
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> throw new NotImplementedException();
/// <inheritdoc/>
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
};
}

24
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);
/// <summary>
/// Reserved. Must always be 0.
/// </summary>
public ushort Reserved = reserved;
public ushort Reserved;
/// <summary>
/// Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid.
/// </summary>
public IconFileType Type = type;
public IconFileType Type;
/// <summary>
/// Specifies number of images in the file.
/// </summary>
public ushort Count = count;
public ushort Count;
public IconDir(IconFileType type)
: this(type, 0)
@ -35,9 +36,16 @@ internal struct IconDir(ushort reserved, IconFileType type, ushort count)
{
}
public static IconDir Parse(ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDir>(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<byte> data)
=> ref Unsafe.As<byte, IconDir>(ref MemoryMarshal.GetReference(data));
public readonly unsafe void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDir, byte>([this]));
public readonly void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
}

9
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
/// </summary>
public uint ImageOffset;
public static IconDirEntry Parse(in ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDirEntry>(data)[0];
public static ref IconDirEntry Parse(in ReadOnlySpan<byte> data)
=> ref Unsafe.As<byte, IconDirEntry>(ref MemoryMarshal.GetReference(data));
public readonly unsafe void WriteTo(in Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDirEntry, byte>([this]));
public readonly void WriteTo(in Stream stream)
=> stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
}

2
src/ImageSharp/Formats/Icon/IconFileType.cs

@ -16,5 +16,5 @@ internal enum IconFileType : ushort
/// <summary>
/// CUR file
/// </summary>
CUR = 2,
CUR = 2
}

6
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);
}
/// <summary>
@ -189,7 +189,7 @@ internal abstract class BitWriterBase
stream.WriteByte(flags);
stream.Write(dataBytes);
RiffHelper.EndWriteChunk(stream, pos);
RiffHelper.EndWriteChunk(stream, pos, 2);
}
/// <summary>

2
src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs

@ -130,6 +130,6 @@ internal readonly struct WebpVp8X : IEquatable<WebpVp8X>
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);
RiffHelper.EndWriteChunk(stream, pos);
RiffHelper.EndWriteChunk(stream, pos, 2);
}
}

2
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -315,7 +315,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition);
RiffHelper.EndWriteChunk(stream, prevPosition, 2);
}
return hasAlpha;

2
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

16
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<byte> FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<uint, byte>(ref Unsafe.AsRef(in this.FourCc)), sizeof(uint));
}

126
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
{
/// <summary>
/// The header bytes identifying RIFF file.
/// </summary>
private const uint RiffFourCc = 0x52_49_46_46;
public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan<byte> data)
{
long pos = BeginWriteChunk(stream, fourCc);
stream.Write(data);
EndWriteChunk(stream, pos);
}
public static void WriteRiffFile(Stream stream, string formType, Action<Stream> func) =>
WriteChunk(stream, RiffFourCc, s =>
{
s.Write(Encoding.ASCII.GetBytes(formType));
func(s);
});
public static void WriteChunk<TStruct>(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<Stream> func)
public static long BeginWriteChunk(Stream stream, ReadOnlySpan<byte> fourCc)
{
Span<byte> 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<byte> data)
public static long BeginWriteChunk(Stream stream, uint fourCc)
{
Span<byte> 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<TStruct>(Stream stream, uint fourCc, in TStruct chunk)
where TStruct : unmanaged
public static long BeginWriteRiff(Stream stream, ReadOnlySpan<byte> formType)
{
fixed (TStruct* ptr = &chunk)
{
WriteChunk(stream, fourCc, new Span<byte>(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<byte> listType)
{
Span<byte> 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<byte> 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<uint, byte>(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;
}
}

26
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<byte> FourCcBytes => MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<uint, byte>(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<byte> data) => ref Unsafe.As<byte, RiffOrListChunkHeader>(ref MemoryMarshal.GetReference(data));
}

15
src/ImageSharp/Formats/Webp/WebpConstants.cs

@ -28,6 +28,11 @@ internal static class WebpConstants
0x2A
];
/// <summary>
/// Gets the header bytes identifying a Webp.
/// </summary>
public const uint WebpFourCc = 0x57_45_42_50;
/// <summary>
/// Signature byte which identifies a VP8L header.
/// </summary>
@ -55,11 +60,6 @@ internal static class WebpConstants
0x50 // P
];
/// <summary>
/// The header bytes identifying a Webp.
/// </summary>
public const string WebpFourCc = "WEBP";
/// <summary>
/// 3 bits reserved for version.
/// </summary>
@ -319,4 +319,9 @@ internal static class WebpConstants
-7, 8,
-8, -9
];
/// <summary>
/// Gets the header bytes identifying a Webp.
/// </summary>
public static ReadOnlySpan<byte> WebpFormTypeFourCc => "WEBP"u8;
}

24
src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs

@ -11,7 +11,7 @@ namespace SixLabors.ImageSharp.Formats.Webp;
public sealed class WebpImageFormatDetector : IImageFormatDetector
{
/// <inheritdoc />
public int HeaderSize => 12;
public int HeaderSize => RiffOrListChunkHeader.HeaderSize;
/// <inheritdoc />
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
@ -21,21 +21,9 @@ public sealed class WebpImageFormatDetector : IImageFormatDetector
}
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize && IsRiffContainer(header) && IsWebpFile(header);
/// <summary>
/// Checks, if the header starts with a valid RIFF FourCC.
/// </summary>
/// <param name="header">The header bytes.</param>
/// <returns>True, if its a valid RIFF FourCC.</returns>
private static bool IsRiffContainer(ReadOnlySpan<byte> header)
=> header[..4].SequenceEqual(WebpConstants.RiffFourCc);
/// <summary>
/// Checks if 'WEBP' is present in the header.
/// </summary>
/// <param name="header">The header bytes.</param>
/// <returns>True, if its a webp file.</returns>
private static bool IsWebpFile(ReadOnlySpan<byte> header)
=> header.Slice(8, 4).SequenceEqual(WebpConstants.WebpHeader);
=> header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is
{
IsRiff: true,
FormType: WebpConstants.WebpFourCc
};
}

41
src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs

@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Ani;
namespace SixLabors.ImageSharp;
@ -102,6 +103,26 @@ public static class ImageMetadataExtensions
/// <returns>The new <see cref="IcoMetadata"/></returns>
public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the <see cref="AniMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="AniMetadata"/>
/// </returns>
public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetIcoMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="IcoMetadata"/></returns>
public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
/// <summary>
/// Gets the <see cref="JpegMetadata"/> from <paramref name="source"/>.<br/>
/// 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
/// <returns>The new <see cref="IcoFrameMetadata"/></returns>
public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the <see cref="AniFrameMetadata"/> from <paramref name="source"/>.<br/>
/// 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.
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>
/// The <see cref="IcoFrameMetadata"/>
/// </returns>
public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetAniMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="IcoFrameMetadata"/></returns>
public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
/// <summary>
/// Gets the <see cref="GifFrameMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata

52
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.
/// <summary>
/// Index within our buffer, not reader position.
/// </summary>
private int readBufferIndex;
// Matches what the stream position would be without buffering
/// <summary>
/// Matches what the stream position would be without buffering
/// </summary>
private long readerPosition;
private bool isDisposed;
@ -194,6 +199,49 @@ internal sealed class BufferedReadStream : Stream
return this.ReadToBufferViaCopyFast(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryReadUnmanaged<T>(out T result)
where T : unmanaged
{
this.cancellationToken.ThrowIfCancellationRequested();
int size = Unsafe.SizeOf<T>();
if (size > this.BufferSize)
{
Span<byte> span = stackalloc byte[size];
if (this.ReadToBufferDirectSlow(span) != size)
{
result = default;
return false;
}
result = MemoryMarshal.Read<T>(span);
}
else
{
if ((uint)this.readBufferIndex > (uint)(this.BufferSize - size))
{
this.FillReadBuffer();
}
if (this.GetCopyCount(size) != size)
{
this.EofHitCount++;
result = default;
return false;
}
Span<byte> span = this.readBuffer.AsSpan(this.readBufferIndex, size);
this.readerPosition += size;
this.readBufferIndex += size;
result = MemoryMarshal.Read<T>(span);
}
return true;
}
/// <inheritdoc/>
public override void Flush()
{

22
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<Rgba32> provider)
{
using Image<Rgba32> image = provider.GetImage(AniDecoder.Instance);
}
}

7
tests/ImageSharp.Tests/TestImages.cs

@ -1379,4 +1379,11 @@ public static class TestImages
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";
}
}

3
tests/Images/Input/Ani/Help.ani

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c49cbb1ca0a3f268695a80df93b1ce2b2cba335a80e8244dd3a702863159bd99
size 12998

3
tests/Images/Input/Ani/Work.ani

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:740353739d3763addddd383614d125918781b8879f7c1ad3c770162a3e143a33
size 1150338

3
tests/Images/Input/Ani/aero_busy.ani

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff38afb523490e1a9f157c0447bc616b19c22df88bdb45c163243d834e9745f8
size 556304
Loading…
Cancel
Save