mirror of https://github.com/SixLabors/ImageSharp
committed by
GitHub
31 changed files with 1195 additions and 129 deletions
@ -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 |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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
|
|||
}; |
|||
} |
|||
@ -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))); |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
}; |
|||
} |
|||
@ -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
|
|||
}; |
|||
} |
|||
@ -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)); |
|||
} |
|||
@ -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)); |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:c49cbb1ca0a3f268695a80df93b1ce2b2cba335a80e8244dd3a702863159bd99 |
|||
size 12998 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:740353739d3763addddd383614d125918781b8879f7c1ad3c770162a3e143a33 |
|||
size 1150338 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:ff38afb523490e1a9f157c0447bc616b19c22df88bdb45c163243d834e9745f8 |
|||
size 556304 |
|||
Loading…
Reference in new issue