mirror of https://github.com/SixLabors/ImageSharp
84 changed files with 3542 additions and 1187 deletions
File diff suppressed because it is too large
@ -0,0 +1,24 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Ani; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes images as Windows animated cursors.
|
||||
|
/// </summary>
|
||||
|
public sealed class AniEncoder : QuantizingImageEncoder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AniEncoder"/> class.
|
||||
|
/// </summary>
|
||||
|
public AniEncoder() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
AniEncoderCore encoder = new(this); |
||||
|
encoder.Encode(image, stream, cancellationToken); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,468 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers; |
||||
|
using System.Buffers.Binary; |
||||
|
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.Memory; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Ani; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Performs ANI encoding.
|
||||
|
/// </summary>
|
||||
|
internal sealed class AniEncoderCore |
||||
|
{ |
||||
|
private readonly AniEncoder encoder; |
||||
|
|
||||
|
// Each nested encoder is configured once and reused for every resource of that type in this ANI operation.
|
||||
|
private IcoEncoderCore? icoEncoder; |
||||
|
private CurEncoderCore? curEncoder; |
||||
|
private BmpEncoderCore? bmpEncoder; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reusable storage for the fixed ANI header and smaller RIFF values.
|
||||
|
/// </summary>
|
||||
|
private InlineArray36<byte> buffer; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AniEncoderCore"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="encoder">The encoder options.</param>
|
||||
|
public AniEncoderCore(AniEncoder encoder) |
||||
|
=> this.encoder = encoder; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes an image as ANI data.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
||||
|
/// <param name="image">The source image.</param>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
||||
|
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Guard.NotNull(image, nameof(image)); |
||||
|
Guard.NotNull(stream, nameof(stream)); |
||||
|
|
||||
|
AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); |
||||
|
AniFrameMetadata firstMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); |
||||
|
AniFrameFormat firstFormat = firstMetadata.FrameFormat; |
||||
|
bool bitmapResources = firstFormat is AniFrameFormat.Bmp; |
||||
|
uint displayRate = firstMetadata.FrameDelay is 0 ? imageMetadata.DisplayRate : firstMetadata.FrameDelay; |
||||
|
bool hasVariableRates = false; |
||||
|
int groupCount = 0; |
||||
|
int maxGroupSize = 1; |
||||
|
|
||||
|
// This validation pass derives the fixed ANI header and largest icon directory without allocating a grouping graph.
|
||||
|
// Encoding repeats the linear grouping scan below, trading a cheap pass for zero per-group collections.
|
||||
|
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
||||
|
{ |
||||
|
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
||||
|
int groupSize = 1; |
||||
|
|
||||
|
// Positive sequence numbers group adjacent resolution variants; non-positive values form independent steps.
|
||||
|
if (metadata.SequenceNumber > 0) |
||||
|
{ |
||||
|
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
||||
|
{ |
||||
|
groupSize++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (bitmapResources != (metadata.FrameFormat is AniFrameFormat.Bmp)) |
||||
|
{ |
||||
|
// AF_ICON applies to the complete file, so raw DIB resources cannot coexist with ICO/CUR resources.
|
||||
|
throw new ImageFormatException("ANI cannot mix bitmap resources with ICO or CUR resources."); |
||||
|
} |
||||
|
|
||||
|
if (bitmapResources && groupSize > 1) |
||||
|
{ |
||||
|
// Only ICO/CUR directories can contain multiple resolution variants in one physical resource.
|
||||
|
throw new ImageFormatException("ANI bitmap resources cannot contain resolution variants."); |
||||
|
} |
||||
|
|
||||
|
// All variants share one animation step, which requires one child format and one rate value.
|
||||
|
for (int i = 1; i < groupSize; i++) |
||||
|
{ |
||||
|
AniFrameMetadata current = image.Frames[frameIndex + i].Metadata.GetAniMetadata(); |
||||
|
if (current.FrameFormat != metadata.FrameFormat) |
||||
|
{ |
||||
|
throw new ImageFormatException("ANI resolution variants must use the same embedded format."); |
||||
|
} |
||||
|
|
||||
|
if (current.FrameDelay != metadata.FrameDelay) |
||||
|
{ |
||||
|
throw new ImageFormatException("ANI resolution variants must use the same frame delay."); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
uint frameDelay = metadata.FrameDelay is 0 ? displayRate : metadata.FrameDelay; |
||||
|
hasVariableRates |= frameDelay != displayRate; |
||||
|
maxGroupSize = Math.Max(maxGroupSize, groupSize); |
||||
|
groupCount++; |
||||
|
frameIndex += groupSize; |
||||
|
} |
||||
|
|
||||
|
// Icon-based ANI files leave global geometry and pixel layout at zero because each ICO/CUR entry owns those values.
|
||||
|
AniHeader header = new() |
||||
|
{ |
||||
|
BytesInHeader = AniHeader.Size, |
||||
|
FrameCount = (uint)groupCount, |
||||
|
StepCount = (uint)groupCount, |
||||
|
Width = bitmapResources ? imageMetadata.Width is 0 ? (uint)image.Width : imageMetadata.Width : 0, |
||||
|
Height = bitmapResources ? imageMetadata.Height is 0 ? (uint)image.Height : imageMetadata.Height : 0, |
||||
|
BitCount = bitmapResources ? imageMetadata.BitCount is 0 ? 32U : imageMetadata.BitCount : 0, |
||||
|
Planes = bitmapResources ? imageMetadata.Planes is 0 ? 1U : imageMetadata.Planes : 0, |
||||
|
DisplayRate = displayRate, |
||||
|
Flags = bitmapResources ? 0 : AniHeaderFlags.IsIcon |
||||
|
}; |
||||
|
|
||||
|
// One allocator-owned directory buffer is sliced and reused for every icon resource; its capacity is the largest group.
|
||||
|
using IMemoryOwner<IconEncoderCore.EncodingFrameMetadata>? iconEntriesOwner = bitmapResources ? null : image.Configuration.MemoryAllocator.Allocate<IconEncoderCore.EncodingFrameMetadata>(maxGroupSize); |
||||
|
Span<IconEncoderCore.EncodingFrameMetadata> iconEntries = iconEntriesOwner is null ? [] : iconEntriesOwner.GetSpan(); |
||||
|
|
||||
|
// ImageEncoder guarantees a seekable destination, allowing direct nested encoding and RIFF size backpatching.
|
||||
|
long riffSizePosition = this.BeginContainer(stream, AniConstants.RiffFourCc, AniConstants.AniFormTypeFourCc); |
||||
|
this.WriteHeader(stream, header); |
||||
|
|
||||
|
if (hasVariableRates) |
||||
|
{ |
||||
|
this.WriteRates(stream, image, displayRate); |
||||
|
} |
||||
|
|
||||
|
if (!this.encoder.SkipMetadata && (imageMetadata.Name is not null || imageMetadata.Artist is not null)) |
||||
|
{ |
||||
|
this.WriteInfoList(stream, imageMetadata, image.Configuration.MemoryAllocator); |
||||
|
} |
||||
|
|
||||
|
long frameListSizePosition = this.BeginContainer(stream, "LIST"u8, "fram"u8); |
||||
|
|
||||
|
// Repeat the allocation-free adjacent grouping scan used by the validation pass.
|
||||
|
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
||||
|
int groupSize = 1; |
||||
|
if (metadata.SequenceNumber > 0) |
||||
|
{ |
||||
|
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
||||
|
{ |
||||
|
groupSize++; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
long frameSizePosition = this.BeginChunk(stream, "icon"u8); |
||||
|
this.WriteFrameResource(image, stream, frameIndex, groupSize, metadata.FrameFormat, header.BitCount, iconEntries, cancellationToken); |
||||
|
this.EndChunk(stream, frameSizePosition); |
||||
|
frameIndex += groupSize; |
||||
|
} |
||||
|
|
||||
|
this.EndChunk(stream, frameListSizePosition); |
||||
|
this.EndChunk(stream, riffSizePosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes the fixed-size ANI animation header chunk.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="header">The animation header.</param>
|
||||
|
private void WriteHeader(Stream stream, AniHeader header) |
||||
|
{ |
||||
|
long sizePosition = this.BeginChunk(stream, "anih"u8); |
||||
|
Span<byte> data = this.buffer; |
||||
|
header.WriteTo(data); |
||||
|
stream.Write(data); |
||||
|
this.EndChunk(stream, sizePosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes per-step rates when they cannot be represented by one header value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="image">The source image.</param>
|
||||
|
/// <param name="displayRate">The default header display rate.</param>
|
||||
|
private void WriteRates(Stream stream, Image image, uint displayRate) |
||||
|
{ |
||||
|
long sizePosition = this.BeginChunk(stream, "rate"u8); |
||||
|
Span<byte> value = this.buffer[..sizeof(uint)]; |
||||
|
|
||||
|
// The rate table contains one DWORD per animation step, not one value per resolution variant.
|
||||
|
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
||||
|
{ |
||||
|
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
||||
|
uint frameDelay = metadata.FrameDelay; |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(value, frameDelay is 0 ? displayRate : frameDelay); |
||||
|
stream.Write(value); |
||||
|
|
||||
|
frameIndex++; |
||||
|
if (metadata.SequenceNumber > 0) |
||||
|
{ |
||||
|
while (frameIndex < image.Frames.Count && image.Frames[frameIndex].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
||||
|
{ |
||||
|
frameIndex++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.EndChunk(stream, sizePosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes the optional ANI name and artist list.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="metadata">The ANI image metadata.</param>
|
||||
|
/// <param name="memoryAllocator">The allocator used for the text buffer.</param>
|
||||
|
private void WriteInfoList(Stream stream, AniMetadata metadata, MemoryAllocator memoryAllocator) |
||||
|
{ |
||||
|
long sizePosition = this.BeginContainer(stream, "LIST"u8, "INFO"u8); |
||||
|
int nameLength = metadata.Name is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Name); |
||||
|
int artistLength = metadata.Artist is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Artist); |
||||
|
|
||||
|
// Name and artist are emitted sequentially, so a single buffer sized for the larger value avoids a second allocation.
|
||||
|
using IMemoryOwner<byte> owner = memoryAllocator.Allocate<byte>(Math.Max(nameLength, artistLength)); |
||||
|
Span<byte> buffer = owner.GetSpan(); |
||||
|
|
||||
|
if (metadata.Name is not null) |
||||
|
{ |
||||
|
this.WriteTextChunk(stream, "INAM"u8, metadata.Name, buffer); |
||||
|
} |
||||
|
|
||||
|
if (metadata.Artist is not null) |
||||
|
{ |
||||
|
this.WriteTextChunk(stream, "IART"u8, metadata.Artist, buffer); |
||||
|
} |
||||
|
|
||||
|
this.EndChunk(stream, sizePosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes a null-terminated ASCII RIFF information chunk.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="fourCc">The chunk identifier.</param>
|
||||
|
/// <param name="value">The text value.</param>
|
||||
|
/// <param name="buffer">The reusable text buffer.</param>
|
||||
|
private void WriteTextChunk(Stream stream, ReadOnlySpan<byte> fourCc, string value, Span<byte> buffer) |
||||
|
{ |
||||
|
long sizePosition = this.BeginChunk(stream, fourCc); |
||||
|
int written = Encoding.ASCII.GetBytes(value, buffer); |
||||
|
stream.Write(buffer[..written]); |
||||
|
|
||||
|
// The terminating zero belongs to the RIFF text payload and is therefore included in the backpatched chunk size.
|
||||
|
stream.WriteByte(0); |
||||
|
|
||||
|
this.EndChunk(stream, sizePosition); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Encodes one ANI frame resource using the existing ICO, CUR, or BMP encoder.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
||||
|
/// <param name="image">The source image.</param>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="frameIndex">The first source-frame index.</param>
|
||||
|
/// <param name="frameCount">The number of source frames in this resource.</param>
|
||||
|
/// <param name="format">The embedded resource format.</param>
|
||||
|
/// <param name="bitCount">The bitmap bit depth declared by the ANI header.</param>
|
||||
|
/// <param name="iconEntries">The reusable icon directory metadata buffer.</param>
|
||||
|
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
||||
|
private void WriteFrameResource<TPixel>(Image<TPixel> image, Stream stream, int frameIndex, int frameCount, AniFrameFormat format, uint bitCount, Span<IconEncoderCore.EncodingFrameMetadata> iconEntries, CancellationToken cancellationToken) |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
switch (format) |
||||
|
{ |
||||
|
case AniFrameFormat.Ico: |
||||
|
case AniFrameFormat.Cur: |
||||
|
// Only the active prefix is exposed to the child encoder; the same backing allocation serves later resources.
|
||||
|
Span<IconEncoderCore.EncodingFrameMetadata> entries = iconEntries[..frameCount]; |
||||
|
AniIconFrameMetadataProvider provider = new(format); |
||||
|
|
||||
|
if (format is AniFrameFormat.Ico) |
||||
|
{ |
||||
|
this.icoEncoder ??= new IcoEncoderCore(new IcoEncoder |
||||
|
{ |
||||
|
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
||||
|
Quantizer = this.encoder.Quantizer, |
||||
|
TransparentColorMode = this.encoder.TransparentColorMode |
||||
|
}); |
||||
|
|
||||
|
this.icoEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
this.curEncoder ??= new CurEncoderCore(new CurEncoder |
||||
|
{ |
||||
|
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
||||
|
Quantizer = this.encoder.Quantizer, |
||||
|
TransparentColorMode = this.encoder.TransparentColorMode |
||||
|
}); |
||||
|
|
||||
|
this.curEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case AniFrameFormat.Bmp: |
||||
|
if (this.bmpEncoder is null) |
||||
|
{ |
||||
|
BmpEncoder bmpEncoder = new() |
||||
|
{ |
||||
|
BitsPerPixel = GetBmpBitsPerPixel(bitCount), |
||||
|
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
||||
|
Quantizer = this.encoder.Quantizer, |
||||
|
SkipFileHeader = true, |
||||
|
SupportTransparency = bitCount is 32, |
||||
|
TransparentColorMode = this.encoder.TransparentColorMode |
||||
|
}; |
||||
|
|
||||
|
this.bmpEncoder = new BmpEncoderCore(bmpEncoder, image.Configuration.MemoryAllocator); |
||||
|
} |
||||
|
|
||||
|
// The frame overload writes the raw DIB directly and avoids constructing a temporary single-frame Image.
|
||||
|
this.bmpEncoder.Encode(image.Frames[frameIndex], image.Metadata, stream, cancellationToken); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates an icon directory entry from ANI-owned frame metadata.
|
||||
|
/// </summary>
|
||||
|
/// <param name="metadata">The ANI frame metadata.</param>
|
||||
|
/// <param name="format">The embedded icon format.</param>
|
||||
|
/// <param name="size">The source frame size.</param>
|
||||
|
/// <returns>The icon directory entry.</returns>
|
||||
|
private static IconDirEntry CreateIconDirEntry(AniFrameMetadata metadata, AniFrameFormat format, Size size) |
||||
|
{ |
||||
|
// PNG and direct-color bitmap entries do not declare a palette; indexed bitmap entries advertise their color count.
|
||||
|
byte colorCount = metadata.Compression is IconFrameCompression.Png || metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 |
||||
|
? (byte)0 |
||||
|
: (byte)ColorNumerics.GetColorCountForBitDepth((int)metadata.BmpBitsPerPixel); |
||||
|
|
||||
|
// ICO stores planes/BPP in these fields, while CUR reuses the same two words for the hotspot coordinates.
|
||||
|
return new IconDirEntry |
||||
|
{ |
||||
|
Width = metadata.EncodingWidth ?? NarrowDimension(size.Width), |
||||
|
Height = metadata.EncodingHeight ?? NarrowDimension(size.Height), |
||||
|
ColorCount = colorCount, |
||||
|
Planes = format is AniFrameFormat.Ico ? (ushort)1 : metadata.HotspotX, |
||||
|
BitCount = format is AniFrameFormat.Ico |
||||
|
? metadata.Compression is IconFrameCompression.Bmp ? (ushort)metadata.BmpBitsPerPixel : (ushort)32 |
||||
|
: metadata.HotspotY |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts an ANI bitmap bit depth to a supported BMP encoder value.
|
||||
|
/// </summary>
|
||||
|
/// <param name="bitCount">The ANI bit depth.</param>
|
||||
|
/// <returns>The BMP encoder bit depth.</returns>
|
||||
|
private static BmpBitsPerPixel GetBmpBitsPerPixel(uint bitCount) |
||||
|
=> bitCount switch |
||||
|
{ |
||||
|
1 => BmpBitsPerPixel.Bit1, |
||||
|
2 => BmpBitsPerPixel.Bit2, |
||||
|
4 => BmpBitsPerPixel.Bit4, |
||||
|
8 => BmpBitsPerPixel.Bit8, |
||||
|
16 => BmpBitsPerPixel.Bit16, |
||||
|
24 => BmpBitsPerPixel.Bit24, |
||||
|
_ => BmpBitsPerPixel.Bit32 |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="value">The pixel dimension.</param>
|
||||
|
/// <returns>The encoded dimension, where zero represents 256 pixels or greater.</returns>
|
||||
|
private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Begins a RIFF chunk whose size will be backpatched after its payload is written.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="fourCc">The chunk identifier.</param>
|
||||
|
/// <returns>The stream position of the chunk-size field.</returns>
|
||||
|
private long BeginChunk(Stream stream, ReadOnlySpan<byte> fourCc) |
||||
|
{ |
||||
|
stream.Write(fourCc); |
||||
|
long sizePosition = stream.Position; |
||||
|
|
||||
|
// Payload length is unknown until nested encoding completes, so reserve the DWORD and remember its absolute position.
|
||||
|
Span<byte> size = this.buffer[..sizeof(uint)]; |
||||
|
size.Clear(); |
||||
|
stream.Write(size); |
||||
|
|
||||
|
return sizePosition; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Begins a RIFF container chunk and writes its form or list type.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="fourCc">The container identifier.</param>
|
||||
|
/// <param name="type">The container form or list type.</param>
|
||||
|
/// <returns>The stream position of the container-size field.</returns>
|
||||
|
private long BeginContainer(Stream stream, ReadOnlySpan<byte> fourCc, ReadOnlySpan<byte> type) |
||||
|
{ |
||||
|
long sizePosition = this.BeginChunk(stream, fourCc); |
||||
|
stream.Write(type); |
||||
|
return sizePosition; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Word-aligns a RIFF chunk and writes its payload size into the reserved field.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The destination stream.</param>
|
||||
|
/// <param name="sizePosition">The stream position of the reserved size field.</param>
|
||||
|
private void EndChunk(Stream stream, long sizePosition) |
||||
|
{ |
||||
|
long endPosition = stream.Position; |
||||
|
|
||||
|
// sizePosition addresses the size DWORD itself; subtracting its four bytes yields payload length.
|
||||
|
uint dataSize = checked((uint)(endPosition - sizePosition - sizeof(uint))); |
||||
|
|
||||
|
// RIFF chunk sizes exclude the optional padding byte used to align the next chunk to a WORD boundary.
|
||||
|
if ((dataSize & 1) is 1) |
||||
|
{ |
||||
|
stream.WriteByte(0); |
||||
|
endPosition++; |
||||
|
} |
||||
|
|
||||
|
Span<byte> size = this.buffer[..sizeof(uint)]; |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(size, dataSize); |
||||
|
|
||||
|
// Backpatch only the reserved DWORD, then restore the append position after any alignment byte.
|
||||
|
stream.Position = sizePosition; |
||||
|
stream.Write(size); |
||||
|
stream.Position = endPosition; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Projects ANI-owned metadata into the icon encoder without allocating intermediary metadata objects.
|
||||
|
/// </summary>
|
||||
|
private readonly struct AniIconFrameMetadataProvider : IconEncoderCore.IEncodingFrameMetadataProvider |
||||
|
{ |
||||
|
private readonly AniFrameFormat format; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AniIconFrameMetadataProvider"/> struct.
|
||||
|
/// </summary>
|
||||
|
/// <param name="format">The embedded icon format.</param>
|
||||
|
public AniIconFrameMetadataProvider(AniFrameFormat format) |
||||
|
=> this.format = format; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IconEncoderCore.EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable) |
||||
|
{ |
||||
|
AniFrameMetadata metadata = frame.Metadata.GetAniMetadata(); |
||||
|
colorTable = metadata.ColorTable; |
||||
|
return new IconEncoderCore.EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, CreateIconDirEntry(metadata, this.format, frame.Size)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Ani; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exposes one ANI frame-resource chunk as an isolated seekable stream.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Embedded decoders accept arbitrary seek offsets from their own headers. Bounding those seeks to the
|
||||
|
/// current RIFF chunk prevents malformed ICO, CUR, or BMP offsets from reading neighboring ANI chunks.
|
||||
|
/// </remarks>
|
||||
|
internal sealed class AniFrameStream : Stream |
||||
|
{ |
||||
|
private readonly Stream stream; |
||||
|
|
||||
|
// start is absolute in the containing stream; position is always relative to this bounded resource.
|
||||
|
private long start; |
||||
|
private long length; |
||||
|
private long position; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="AniFrameStream"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The containing ANI stream.</param>
|
||||
|
public AniFrameStream(Stream stream) |
||||
|
=> this.stream = stream; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanRead => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanSeek => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanWrite => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Length => this.length; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Position |
||||
|
{ |
||||
|
get => this.position; |
||||
|
set => this.Seek(value, SeekOrigin.Begin); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Repositions this stream over another frame-resource payload in the same containing stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The absolute start of the frame-resource payload.</param>
|
||||
|
/// <param name="length">The frame-resource payload length.</param>
|
||||
|
public void Reset(long start, long length) |
||||
|
{ |
||||
|
this.start = start; |
||||
|
this.length = length; |
||||
|
this.position = 0; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Flush() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int Read(byte[] buffer, int offset, int count) |
||||
|
=> this.Read(buffer.AsSpan(offset, count)); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int Read(Span<byte> buffer) |
||||
|
{ |
||||
|
// Clamp every read to the resource boundary so a child decoder cannot consume the next RIFF chunk.
|
||||
|
int count = (int)Math.Min(buffer.Length, this.length - this.position); |
||||
|
if (count is 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// The containing stream is shared by all resources, so synchronize its absolute position immediately before reading.
|
||||
|
this.stream.Position = this.start + this.position; |
||||
|
int read = this.stream.Read(buffer[..count]); |
||||
|
this.position += read; |
||||
|
|
||||
|
return read; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Seek(long offset, SeekOrigin origin) |
||||
|
{ |
||||
|
long target = origin switch |
||||
|
{ |
||||
|
SeekOrigin.Begin => offset, |
||||
|
SeekOrigin.Current => this.position + offset, |
||||
|
SeekOrigin.End => this.length + offset, |
||||
|
_ => throw new ArgumentOutOfRangeException(nameof(origin)) |
||||
|
}; |
||||
|
|
||||
|
// Casting rejects both negative offsets and offsets beyond Length with one bounds check.
|
||||
|
if ((ulong)target > (ulong)this.length) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The embedded ANI frame resource contains an invalid seek offset."); |
||||
|
} |
||||
|
|
||||
|
// Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resource processing.
|
||||
|
this.position = target; |
||||
|
return target; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void SetLength(long value) => throw new NotSupportedException(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
||||
|
} |
||||
@ -1,32 +1,98 @@ |
|||||
// Copyright (c) Six Labors.
|
// Copyright (c) Six Labors.
|
||||
// Licensed under the Six Labors Split License.
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
using System.Runtime.CompilerServices; |
using System.Buffers.Binary; |
||||
using System.Runtime.InteropServices; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Formats.Ani; |
namespace SixLabors.ImageSharp.Formats.Ani; |
||||
|
|
||||
internal readonly struct AniHeader |
/// <summary>
|
||||
|
/// Represents the data stored in an ANI "anih" chunk.
|
||||
|
/// </summary>
|
||||
|
internal struct AniHeader |
||||
{ |
{ |
||||
public uint Size { get; } |
/// <summary>
|
||||
|
/// The number of bytes in the ANI header.
|
||||
|
/// </summary>
|
||||
|
public const int Size = 9 * sizeof(uint); |
||||
|
|
||||
public uint Frames { get; } |
/// <summary>
|
||||
|
/// Gets or sets the declared ANI header size.
|
||||
|
/// </summary>
|
||||
|
public uint BytesInHeader { get; set; } |
||||
|
|
||||
public uint Steps { get; } |
/// <summary>
|
||||
|
/// Gets or sets the number of embedded frame resources.
|
||||
|
/// </summary>
|
||||
|
public uint FrameCount { get; set; } |
||||
|
|
||||
public uint Width { get; } |
/// <summary>
|
||||
|
/// Gets or sets the number of animation steps.
|
||||
|
/// </summary>
|
||||
|
public uint StepCount { get; set; } |
||||
|
|
||||
public uint Height { get; } |
/// <summary>
|
||||
|
/// Gets or sets the frame width used by bitmap-based animations.
|
||||
|
/// </summary>
|
||||
|
public uint Width { get; set; } |
||||
|
|
||||
public uint BitCount { get; } |
/// <summary>
|
||||
|
/// Gets or sets the frame height used by bitmap-based animations.
|
||||
|
/// </summary>
|
||||
|
public uint Height { get; set; } |
||||
|
|
||||
public uint Planes { get; } |
/// <summary>
|
||||
|
/// Gets or sets the encoded bits per pixel.
|
||||
|
/// </summary>
|
||||
|
public uint BitCount { get; set; } |
||||
|
|
||||
public uint DisplayRate { get; } |
/// <summary>
|
||||
|
/// Gets or sets the number of color planes.
|
||||
|
/// </summary>
|
||||
|
public uint Planes { get; set; } |
||||
|
|
||||
public AniHeaderFlags Flags { get; } |
/// <summary>
|
||||
|
/// Gets or sets the default display rate in sixtieths of a second.
|
||||
|
/// </summary>
|
||||
|
public uint DisplayRate { get; set; } |
||||
|
|
||||
public static ref AniHeader Parse(ReadOnlySpan<byte> data) => ref Unsafe.As<byte, AniHeader>(ref MemoryMarshal.GetReference(data)); |
/// <summary>
|
||||
|
/// Gets or sets the ANI header flags.
|
||||
|
/// </summary>
|
||||
|
public AniHeaderFlags Flags { get; set; } |
||||
|
|
||||
public void WriteTo(Stream stream) => stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1))); |
/// <summary>
|
||||
|
/// Parses an ANI header from its little-endian byte representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The ANI header data.</param>
|
||||
|
/// <returns>The parsed ANI header.</returns>
|
||||
|
public static AniHeader Parse(ReadOnlySpan<byte> data) |
||||
|
=> new() |
||||
|
{ |
||||
|
BytesInHeader = BinaryPrimitives.ReadUInt32LittleEndian(data), |
||||
|
FrameCount = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]), |
||||
|
StepCount = BinaryPrimitives.ReadUInt32LittleEndian(data[8..]), |
||||
|
Width = BinaryPrimitives.ReadUInt32LittleEndian(data[12..]), |
||||
|
Height = BinaryPrimitives.ReadUInt32LittleEndian(data[16..]), |
||||
|
BitCount = BinaryPrimitives.ReadUInt32LittleEndian(data[20..]), |
||||
|
Planes = BinaryPrimitives.ReadUInt32LittleEndian(data[24..]), |
||||
|
DisplayRate = BinaryPrimitives.ReadUInt32LittleEndian(data[28..]), |
||||
|
Flags = (AniHeaderFlags)BinaryPrimitives.ReadUInt32LittleEndian(data[32..]) |
||||
|
}; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Writes the ANI header to its little-endian byte representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="destination">The destination buffer.</param>
|
||||
|
public readonly void WriteTo(Span<byte> destination) |
||||
|
{ |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination, this.BytesInHeader); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[4..], this.FrameCount); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[8..], this.StepCount); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[12..], this.Width); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[16..], this.Height); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[20..], this.BitCount); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[24..], this.Planes); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[28..], this.DisplayRate); |
||||
|
BinaryPrimitives.WriteUInt32LittleEndian(destination[32..], (uint)this.Flags); |
||||
|
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,34 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Ani; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a RIFF chunk identifier and payload size.
|
||||
|
/// </summary>
|
||||
|
internal struct AniRiffChunkHeader |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the chunk identifier.
|
||||
|
/// </summary>
|
||||
|
public uint FourCc { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the chunk payload size in bytes, excluding alignment padding.
|
||||
|
/// </summary>
|
||||
|
public uint Size { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Parses a RIFF chunk header from its little-endian byte representation.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The RIFF chunk header data.</param>
|
||||
|
/// <returns>The parsed RIFF chunk header.</returns>
|
||||
|
public static AniRiffChunkHeader Parse(ReadOnlySpan<byte> data) |
||||
|
=> new() |
||||
|
{ |
||||
|
FourCc = BinaryPrimitives.ReadUInt32LittleEndian(data), |
||||
|
Size = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]) |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using SixLabors.ImageSharp.Formats.Icon; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Cur; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Detects CUR file headers.
|
||||
|
/// </summary>
|
||||
|
public sealed class CurImageFormatDetector : IImageFormatDetector |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public int HeaderSize => IconDir.Size + IconDirEntry.Size; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
||||
|
{ |
||||
|
format = this.IsSupportedFileFormat(header) ? CurFormat.Instance : null; |
||||
|
return format is not null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether the supplied header contains a valid CUR directory and first entry.
|
||||
|
/// </summary>
|
||||
|
/// <param name="header">The candidate file header.</param>
|
||||
|
/// <returns><see langword="true"/> when the header identifies CUR data.</returns>
|
||||
|
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) |
||||
|
{ |
||||
|
if (header.Length < this.HeaderSize) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
IconDir dir = IconDir.Parse(header); |
||||
|
if (dir is not { Reserved: 0, Type: IconFileType.CUR } || dir.Count is 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); |
||||
|
|
||||
|
// The first payload must begin after the complete directory, even when the caller supplied only the detection prefix.
|
||||
|
return entry.Reserved is 0 |
||||
|
&& entry.BytesInRes is not 0 |
||||
|
&& entry.ImageOffset >= IconDir.Size + (dir.Count * IconDirEntry.Size); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Diagnostics.CodeAnalysis; |
||||
|
using SixLabors.ImageSharp.Formats.Icon; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Ico; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Detects ICO file headers.
|
||||
|
/// </summary>
|
||||
|
public sealed class IcoImageFormatDetector : IImageFormatDetector |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public int HeaderSize => IconDir.Size + IconDirEntry.Size; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
||||
|
{ |
||||
|
format = this.IsSupportedFileFormat(header) ? IcoFormat.Instance : null; |
||||
|
return format is not null; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether the supplied header contains a valid ICO directory and first entry.
|
||||
|
/// </summary>
|
||||
|
/// <param name="header">The candidate file header.</param>
|
||||
|
/// <returns><see langword="true"/> when the header identifies ICO data.</returns>
|
||||
|
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) |
||||
|
{ |
||||
|
if (header.Length < this.HeaderSize) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
IconDir dir = IconDir.Parse(header); |
||||
|
if (dir is not { Reserved: 0, Type: IconFileType.ICO } || dir.Count is 0) |
||||
|
{ |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); |
||||
|
|
||||
|
// The first payload must begin after the complete directory, even when the caller supplied only the detection prefix.
|
||||
|
return entry is { Reserved: 0, Planes: 0 or 1, BitCount: 1 or 4 or 8 or 16 or 24 or 32 } |
||||
|
&& entry.BytesInRes is not 0 |
||||
|
&& entry.ImageOffset >= IconDir.Size + (dir.Count * IconDirEntry.Size); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Formats.Icon; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exposes one ICO or CUR directory entry as an isolated seekable stream.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Embedded decoders accept seek offsets from their own headers. Bounding those seeks to <c>BytesInRes</c>
|
||||
|
/// prevents a malformed BMP or PNG payload from consuming an adjacent icon resource.
|
||||
|
/// </remarks>
|
||||
|
internal sealed class IconFrameStream : Stream |
||||
|
{ |
||||
|
private readonly Stream stream; |
||||
|
|
||||
|
// start is absolute in the containing stream; position is always relative to this bounded resource.
|
||||
|
private long start; |
||||
|
private long length; |
||||
|
private long position; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="IconFrameStream"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The containing icon stream.</param>
|
||||
|
public IconFrameStream(Stream stream) |
||||
|
=> this.stream = stream; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanRead => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanSeek => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override bool CanWrite => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Length => this.length; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Position |
||||
|
{ |
||||
|
get => this.position; |
||||
|
set => this.Seek(value, SeekOrigin.Begin); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Repositions this stream over another image payload in the same containing stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="start">The absolute start of the image payload.</param>
|
||||
|
/// <param name="length">The image payload length.</param>
|
||||
|
public void Reset(long start, long length) |
||||
|
{ |
||||
|
this.start = start; |
||||
|
this.length = length; |
||||
|
this.position = 0; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Flush() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int Read(byte[] buffer, int offset, int count) |
||||
|
=> this.Read(buffer.AsSpan(offset, count)); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override int Read(Span<byte> buffer) |
||||
|
{ |
||||
|
// Clamp every read to the entry boundary so a child decoder cannot consume the next resource.
|
||||
|
int count = (int)Math.Min(buffer.Length, this.length - this.position); |
||||
|
if (count is 0) |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
// The containing stream is shared by all entries, so synchronize its absolute position immediately before reading.
|
||||
|
this.stream.Position = this.start + this.position; |
||||
|
int read = this.stream.Read(buffer[..count]); |
||||
|
this.position += read; |
||||
|
|
||||
|
return read; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override long Seek(long offset, SeekOrigin origin) |
||||
|
{ |
||||
|
long target = origin switch |
||||
|
{ |
||||
|
SeekOrigin.Begin => offset, |
||||
|
SeekOrigin.Current => this.position + offset, |
||||
|
SeekOrigin.End => this.length + offset, |
||||
|
_ => throw new ArgumentOutOfRangeException(nameof(origin)) |
||||
|
}; |
||||
|
|
||||
|
// Casting rejects both negative offsets and offsets beyond Length with one bounds check.
|
||||
|
if ((ulong)target > (ulong)this.length) |
||||
|
{ |
||||
|
throw new InvalidImageContentException("The embedded icon resource contains an invalid seek offset."); |
||||
|
} |
||||
|
|
||||
|
// Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resources.
|
||||
|
this.position = target; |
||||
|
return target; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void SetLength(long value) => throw new NotSupportedException(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
||||
|
} |
||||
@ -1,66 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Formats.Icon; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Detects ico file headers.
|
|
||||
/// </summary>
|
|
||||
public class IconImageFormatDetector : IImageFormatDetector |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public int HeaderSize { get; } = IconDir.Size + IconDirEntry.Size; |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
|
||||
{ |
|
||||
format = this.IsSupportedFileFormat(header) switch |
|
||||
{ |
|
||||
true => Ico.IcoFormat.Instance, |
|
||||
false => Cur.CurFormat.Instance, |
|
||||
null => default |
|
||||
}; |
|
||||
|
|
||||
return format is not null; |
|
||||
} |
|
||||
|
|
||||
private bool? IsSupportedFileFormat(ReadOnlySpan<byte> header) |
|
||||
{ |
|
||||
// There are no magic bytes in the first few bytes of a tga file,
|
|
||||
// so we try to figure out if its a valid tga by checking for valid tga header bytes.
|
|
||||
if (header.Length < this.HeaderSize) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
IconDir dir = IconDir.Parse(header); |
|
||||
if (dir is not { Reserved: 0 } // Should be 0.
|
|
||||
or not { Type: IconFileType.ICO or IconFileType.CUR } // Unknown Type.
|
|
||||
or { Count: 0 }) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
IconDirEntry entry = IconDirEntry.Parse(header[IconDir.Size..]); |
|
||||
if (entry is not { Reserved: 0 } // Should be 0.
|
|
||||
or { BytesInRes: 0 } // Should not be 0.
|
|
||||
|| entry.ImageOffset < IconDir.Size + (dir.Count * IconDirEntry.Size)) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
if (dir.Type is IconFileType.ICO) |
|
||||
{ |
|
||||
if (entry is not { BitCount: 1 or 4 or 8 or 16 or 24 or 32 } or not { Planes: 0 or 1 }) |
|
||||
{ |
|
||||
return null; |
|
||||
} |
|
||||
|
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
@ -1,16 +0,0 @@ |
|||||
// 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)); |
|
||||
} |
|
||||
@ -1,26 +0,0 @@ |
|||||
// 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,156 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
using SixLabors.ImageSharp.Formats.Ani; |
||||
|
using SixLabors.ImageSharp.Formats.Icon; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; |
||||
|
using static SixLabors.ImageSharp.Tests.TestImages.Ani; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Ani; |
||||
|
|
||||
|
[Trait("Format", "Ani")] |
||||
|
[ValidateDisposedMemoryAllocations] |
||||
|
public class AniEncoderTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies that ANI resources, including multi-resolution CUR resources, survive an encode/decode round trip.
|
||||
|
/// </summary>
|
||||
|
[Theory] |
||||
|
[WithFile(Work, PixelTypes.Rgba32)] |
||||
|
[WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)] |
||||
|
[WithFile(Help, PixelTypes.Rgba32)] |
||||
|
public void AniEncoder_RoundTrips(TestImageProvider<Rgba32> provider) |
||||
|
{ |
||||
|
using Image<Rgba32> image = provider.GetImage(AniDecoder.Instance); |
||||
|
using MemoryStream stream = new(); |
||||
|
|
||||
|
image.Save(stream, new AniEncoder()); |
||||
|
|
||||
|
// The RIFF size covers everything after its identifier and size field.
|
||||
|
Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); |
||||
|
|
||||
|
stream.Position = 0; |
||||
|
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); |
||||
|
|
||||
|
ImageComparer.Exact.VerifySimilarity(image, decoded); |
||||
|
Assert.Equal(image.Frames.Count, decoded.Frames.Count); |
||||
|
|
||||
|
for (int i = 0; i < image.Frames.Count; i++) |
||||
|
{ |
||||
|
AniFrameMetadata expected = image.Frames[i].Metadata.GetAniMetadata(); |
||||
|
AniFrameMetadata actual = decoded.Frames[i].Metadata.GetAniMetadata(); |
||||
|
|
||||
|
Assert.Equal(expected.SequenceNumber, actual.SequenceNumber); |
||||
|
Assert.Equal(expected.FrameDelay, actual.FrameDelay); |
||||
|
Assert.Equal(expected.FrameFormat, actual.FrameFormat); |
||||
|
Assert.Equal(expected.EncodingWidth, actual.EncodingWidth); |
||||
|
Assert.Equal(expected.EncodingHeight, actual.EncodingHeight); |
||||
|
Assert.Equal(expected.Compression, actual.Compression); |
||||
|
Assert.Equal(expected.BmpBitsPerPixel, actual.BmpBitsPerPixel); |
||||
|
Assert.Equal(expected.HotspotX, actual.HotspotX); |
||||
|
Assert.Equal(expected.HotspotY, actual.HotspotY); |
||||
|
Assert.Equal(expected.ColorTable?.ToArray(), actual.ColorTable?.ToArray()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that per-step rates and RIFF information metadata are emitted and decoded.
|
||||
|
/// </summary>
|
||||
|
[Theory] |
||||
|
[WithFile(Work, PixelTypes.Rgba32)] |
||||
|
public void AniEncoder_WritesVariableRatesAndInformation(TestImageProvider<Rgba32> provider) |
||||
|
{ |
||||
|
using Image<Rgba32> image = provider.GetImage(AniDecoder.Instance); |
||||
|
AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); |
||||
|
imageMetadata.Name = "ImageSharp ANI"; |
||||
|
imageMetadata.Artist = "Six Labors"; |
||||
|
|
||||
|
for (int i = 0; i < image.Frames.Count; i++) |
||||
|
{ |
||||
|
image.Frames[i].Metadata.GetAniMetadata().FrameDelay = (uint)(i + 1); |
||||
|
} |
||||
|
|
||||
|
using MemoryStream stream = new(); |
||||
|
image.Save(stream, new AniEncoder()); |
||||
|
|
||||
|
Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); |
||||
|
|
||||
|
stream.Position = 0; |
||||
|
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); |
||||
|
AniMetadata decodedMetadata = decoded.Metadata.GetAniMetadata(); |
||||
|
|
||||
|
Assert.Equal(imageMetadata.Name, decodedMetadata.Name); |
||||
|
Assert.Equal(imageMetadata.Artist, decodedMetadata.Artist); |
||||
|
|
||||
|
for (int i = 0; i < decoded.Frames.Count; i++) |
||||
|
{ |
||||
|
Assert.Equal((uint)(i + 1), decoded.Frames[i].Metadata.GetAniMetadata().FrameDelay); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies the encoder and decoder paths for embedded ICO and BMP resources.
|
||||
|
/// </summary>
|
||||
|
[Theory] |
||||
|
[InlineData(AniFrameFormat.Ico)] |
||||
|
[InlineData(AniFrameFormat.Bmp)] |
||||
|
public void AniEncoder_RoundTripsOtherFrameFormats(AniFrameFormat frameFormat) |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(16, 16, Color.Red.ToPixel<Rgba32>()); |
||||
|
AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); |
||||
|
imageMetadata.DisplayRate = 6; |
||||
|
imageMetadata.BitCount = 32; |
||||
|
imageMetadata.Planes = 1; |
||||
|
|
||||
|
AniFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); |
||||
|
frameMetadata.FrameDelay = 6; |
||||
|
frameMetadata.SequenceNumber = 1; |
||||
|
frameMetadata.FrameFormat = frameFormat; |
||||
|
frameMetadata.Compression = IconFrameCompression.Bmp; |
||||
|
|
||||
|
using MemoryStream stream = new(); |
||||
|
image.Save(stream, new AniEncoder()); |
||||
|
|
||||
|
Assert.Equal(stream.Length - 8, BinaryPrimitives.ReadUInt32LittleEndian(stream.GetBuffer().AsSpan(4, sizeof(uint)))); |
||||
|
|
||||
|
if (frameFormat is AniFrameFormat.Bmp) |
||||
|
{ |
||||
|
ReadOnlySpan<byte> encoded = stream.GetBuffer().AsSpan(0, (int)stream.Length); |
||||
|
int frameChunkOffset = encoded.IndexOf("icon"u8); |
||||
|
Assert.True(frameChunkOffset >= 0); |
||||
|
|
||||
|
// AF_ICON-clear resources contain a headerless BMP DIB, not a standalone file beginning with BITMAPFILEHEADER.
|
||||
|
ReadOnlySpan<byte> frameData = encoded[(frameChunkOffset + AniConstants.ChunkHeaderSize)..]; |
||||
|
Assert.False(frameData.StartsWith("BM"u8)); |
||||
|
} |
||||
|
|
||||
|
stream.Position = 0; |
||||
|
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); |
||||
|
|
||||
|
ImageComparer.Exact.VerifySimilarity(image, decoded); |
||||
|
Assert.Equal(frameFormat, decoded.Frames.RootFrame.Metadata.GetAniMetadata().FrameFormat); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that an independent frame cannot collide with an explicit sequence group.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void AniEncoder_NonPositiveSequenceDoesNotCollideWithExplicitGroup() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(16, 16, Color.Red.ToPixel<Rgba32>()); |
||||
|
image.Frames.AddFrame(image.Frames.RootFrame); |
||||
|
image.Frames[1].Metadata.GetAniMetadata().SequenceNumber = 1; |
||||
|
|
||||
|
using MemoryStream stream = new(); |
||||
|
image.Save(stream, new AniEncoder()); |
||||
|
|
||||
|
stream.Position = 0; |
||||
|
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream); |
||||
|
|
||||
|
Assert.Equal(2, decoded.Frames.Count); |
||||
|
Assert.Equal(1, decoded.Frames[0].Metadata.GetAniMetadata().SequenceNumber); |
||||
|
Assert.Equal(2, decoded.Frames[1].Metadata.GetAniMetadata().SequenceNumber); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Ani; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Processing; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Ani; |
||||
|
|
||||
|
[Trait("Format", "Ani")] |
||||
|
public class AniMetadataTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Verifies that resizing scales the ANI-owned encoding dimensions exactly once.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void AfterFrameApply_ScalesEncodingDimensionsOnce() |
||||
|
{ |
||||
|
using Image<Rgba32> image = new(32, 32); |
||||
|
AniFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); |
||||
|
metadata.EncodingWidth = 32; |
||||
|
metadata.EncodingHeight = 32; |
||||
|
metadata.FrameFormat = AniFrameFormat.Cur; |
||||
|
|
||||
|
image.Mutate(context => context.Resize(64, 64)); |
||||
|
|
||||
|
AniFrameMetadata resized = image.Frames.RootFrame.Metadata.GetAniMetadata(); |
||||
|
Assert.Equal((byte)64, resized.EncodingWidth); |
||||
|
Assert.Equal((byte)64, resized.EncodingHeight); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue