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