Browse Source

Complete ANI, ICO, and CUR codec support

pull/2899/head
James Jackson-South 3 weeks ago
parent
commit
db5b03d894
  1. 52
      src/ImageSharp/Common/InlineArray.cs
  2. 2
      src/ImageSharp/Common/InlineArray.tt
  3. 15
      src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs
  4. 11
      src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs
  5. 5
      src/ImageSharp/Configuration.cs
  6. 41
      src/ImageSharp/Formats/Ani/AniChunkType.cs
  7. 11
      src/ImageSharp/Formats/Ani/AniConfigurationModule.cs
  8. 28
      src/ImageSharp/Formats/Ani/AniConstants.cs
  9. 19
      src/ImageSharp/Formats/Ani/AniDecoder.cs
  10. 947
      src/ImageSharp/Formats/Ani/AniDecoderCore.cs
  11. 24
      src/ImageSharp/Formats/Ani/AniEncoder.cs
  12. 468
      src/ImageSharp/Formats/Ani/AniEncoderCore.cs
  13. 9
      src/ImageSharp/Formats/Ani/AniFormat.cs
  14. 12
      src/ImageSharp/Formats/Ani/AniFrameFormat.cs
  15. 223
      src/ImageSharp/Formats/Ani/AniFrameMetadata.cs
  16. 114
      src/ImageSharp/Formats/Ani/AniFrameStream.cs
  17. 94
      src/ImageSharp/Formats/Ani/AniHeader.cs
  18. 6
      src/ImageSharp/Formats/Ani/AniHeaderFlags.cs
  19. 27
      src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs
  20. 96
      src/ImageSharp/Formats/Ani/AniMetadata.cs
  21. 34
      src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs
  22. 70
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  23. 6
      src/ImageSharp/Formats/Cur/CurConfigurationModule.cs
  24. 10
      src/ImageSharp/Formats/Cur/CurConstants.cs
  25. 7
      src/ImageSharp/Formats/Cur/CurDecoder.cs
  26. 10
      src/ImageSharp/Formats/Cur/CurDecoderCore.cs
  27. 5
      src/ImageSharp/Formats/Cur/CurEncoder.cs
  28. 33
      src/ImageSharp/Formats/Cur/CurEncoderCore.cs
  29. 9
      src/ImageSharp/Formats/Cur/CurFormat.cs
  30. 52
      src/ImageSharp/Formats/Cur/CurFrameMetadata.cs
  31. 49
      src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs
  32. 6
      src/ImageSharp/Formats/Cur/CurMetadata.cs
  33. 32
      src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
  34. 2
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  35. 32
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  36. 30
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  37. 6
      src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs
  38. 10
      src/ImageSharp/Formats/Ico/IcoConstants.cs
  39. 7
      src/ImageSharp/Formats/Ico/IcoDecoder.cs
  40. 18
      src/ImageSharp/Formats/Ico/IcoDecoderCore.cs
  41. 5
      src/ImageSharp/Formats/Ico/IcoEncoder.cs
  42. 33
      src/ImageSharp/Formats/Ico/IcoEncoderCore.cs
  43. 5
      src/ImageSharp/Formats/Ico/IcoFormat.cs
  44. 49
      src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs
  45. 49
      src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs
  46. 6
      src/ImageSharp/Formats/Ico/IcoMetadata.cs
  47. 415
      src/ImageSharp/Formats/Icon/IconDecoderCore.cs
  48. 38
      src/ImageSharp/Formats/Icon/IconDir.cs
  49. 25
      src/ImageSharp/Formats/Icon/IconDirEntry.cs
  50. 248
      src/ImageSharp/Formats/Icon/IconEncoderCore.cs
  51. 6
      src/ImageSharp/Formats/Icon/IconFileType.cs
  52. 6
      src/ImageSharp/Formats/Icon/IconFrameCompression.cs
  53. 114
      src/ImageSharp/Formats/Icon/IconFrameStream.cs
  54. 66
      src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs
  55. 7
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs
  56. 12
      src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs
  57. 42
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  58. 6
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  59. 2
      src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
  60. 19
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  61. 10
      src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs
  62. 12
      src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs
  63. 2
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  64. 12
      src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs
  65. 2
      src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs
  66. 8
      src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs
  67. 16
      src/ImageSharp/Formats/Webp/RiffChunkHeader.cs
  68. 126
      src/ImageSharp/Formats/Webp/RiffHelper.cs
  69. 26
      src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs
  70. 15
      src/ImageSharp/Formats/Webp/WebpConstants.cs
  71. 24
      src/ImageSharp/Formats/Webp/WebpImageFormatDetector.cs
  72. 103
      src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs
  73. 78
      src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs
  74. 2
      src/ImageSharp/Formats/_Generated/_Formats.ttinclude
  75. 52
      src/ImageSharp/IO/BufferedReadStream.cs
  76. 6
      tests/Directory.Build.targets
  77. 2
      tests/ImageSharp.Tests/ConfigurationTests.cs
  78. 134
      tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs
  79. 156
      tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs
  80. 31
      tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs
  81. 40
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs
  82. 36
      tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs
  83. 48
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  84. 3
      tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

52
src/ImageSharp/Common/InlineArray.cs

@ -26,6 +26,15 @@ internal struct InlineArray8<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 14 elements.
/// </summary>
[InlineArray(14)]
internal struct InlineArray14<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 16 elements.
/// </summary>
@ -35,4 +44,47 @@ internal struct InlineArray16<T>
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 18 elements.
/// </summary>
[InlineArray(18)]
internal struct InlineArray18<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 19 elements.
/// </summary>
[InlineArray(19)]
internal struct InlineArray19<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 26 elements.
/// </summary>
[InlineArray(26)]
internal struct InlineArray26<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 36 elements.
/// </summary>
[InlineArray(36)]
internal struct InlineArray36<T>
{
private T t;
}
/// <summary>
/// Represents a safe, fixed sized buffer of 256 elements.
/// </summary>
[InlineArray(256)]
internal struct InlineArray256<T>
{
private T t;
}

2
src/ImageSharp/Common/InlineArray.tt

@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp;
<#GenerateInlineArrays();#>
<#+
private static int[] Lengths = [4, 8, 16 ];
private static int[] Lengths = [4, 8, 14, 16, 18, 19, 26, 36, 256];
void GenerateInlineArrays()
{

15
src/ImageSharp/Compression/Zlib/ChunkedReadStream.cs

@ -77,13 +77,14 @@ internal sealed class ChunkedReadStream : Stream
}
/// <inheritdoc/>
public override int Read(byte[] buffer, int offset, int count)
public override int Read(byte[] buffer, int offset, int count) => this.Read(buffer.AsSpan(offset, count));
/// <inheritdoc/>
public override int Read(Span<byte> buffer)
{
// Decrement currentDataRemaining only by bytes actually returned by
// innerStream.Read; a short read otherwise underflows the segment
// counter and triggers getData() before the segment is truly drained.
// Decrement currentDataRemaining only by bytes actually returned by innerStream.Read; a short read otherwise advances segments too early.
int totalBytesRead = 0;
while (totalBytesRead < count)
while (totalBytesRead < buffer.Length)
{
if (this.currentDataRemaining is 0)
{
@ -94,8 +95,8 @@ internal sealed class ChunkedReadStream : Stream
}
}
int bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer, offset + totalBytesRead, bytesToRead);
int bytesToRead = Math.Min(buffer.Length - totalBytesRead, this.currentDataRemaining);
int bytesRead = this.innerStream.Read(buffer.Slice(totalBytesRead, bytesToRead));
if (bytesRead is 0)
{
break;

11
src/ImageSharp/Compression/Zlib/ZlibInflateReader.cs

@ -14,13 +14,6 @@ namespace SixLabors.ImageSharp.Compression.Zlib;
/// </summary>
internal sealed class ZlibInflateReader : IDisposable
{
/// <summary>
/// Used to read the Adler-32 and Crc-32 checksums.
/// We don't actually use this for anything so it doesn't
/// have to be threadsafe.
/// </summary>
private static readonly byte[] ChecksumBuffer = new byte[4];
private readonly ChunkedReadStream segmentStream;
public ZlibInflateReader(BufferedReadStream innerStream)
@ -112,7 +105,9 @@ internal sealed class ZlibInflateReader : IDisposable
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
if (this.segmentStream.Read(ChecksumBuffer, 0, 4) != 4)
InlineArray4<byte> checksumBuffer = default;
if (this.segmentStream.Read(checksumBuffer) != 4)
{
return false;
}

5
src/ImageSharp/Configuration.cs

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Exr;
@ -224,6 +225,7 @@ public sealed class Configuration
/// <see cref="WebpConfigurationModule"/>.
/// <see cref="ExrConfigurationModule"/>.
/// <see cref="QoiConfigurationModule"/>.
/// <see cref="AniConfigurationModule"/>.
/// </summary>
/// <returns>The default configuration of <see cref="Configuration"/>.</returns>
internal static Configuration CreateDefaultInstance() => new(
@ -238,5 +240,6 @@ public sealed class Configuration
new ExrConfigurationModule(),
new QoiConfigurationModule(),
new IcoConfigurationModule(),
new CurConfigurationModule());
new CurConfigurationModule(),
new AniConfigurationModule());
}

41
src/ImageSharp/Formats/Ani/AniChunkType.cs

@ -3,68 +3,71 @@
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Identifies top-level ANI RIFF chunks.
/// </summary>
internal enum AniChunkType : uint
{
/// <summary>
/// "anih"
/// The animation header chunk, "anih".
/// </summary>
AniH = 0x68_69_6E_61,
Header = 0x68_69_6E_61,
/// <summary>
/// "seq "
/// The frame sequence chunk, "seq ".
/// </summary>
Seq = 0x20_71_65_73,
Sequence = 0x20_71_65_73,
/// <summary>
/// "rate"
/// The per-step display-rate chunk, "rate".
/// </summary>
Rate = 0x65_74_61_72,
/// <summary>
/// "LIST"
/// A RIFF list chunk, "LIST".
/// </summary>
List = 0x54_53_49_4C
}
/// <summary>
/// ListType
/// Identifies ANI RIFF list types.
/// </summary>
internal enum AniListType : uint
{
/// <summary>
/// "INFO" (ListType)
/// The information list, "INFO".
/// </summary>
Info = 0x4F_46_4E_49,
/// <summary>
/// "fram"
/// The embedded frame-resource list, "fram".
/// </summary>
Fram = 0x6D_61_72_66
Frames = 0x6D_61_72_66
}
/// <summary>
/// in "INFO"
/// Identifies chunks stored in an ANI information list.
/// </summary>
internal enum AniListInfoType : uint
internal enum AniInfoChunkType : uint
{
/// <summary>
/// "INAM"
/// The animation name, "INAM".
/// </summary>
INam = 0x4D_41_4E_49,
Name = 0x4D_41_4E_49,
/// <summary>
/// "IART"
/// The animation artist, "IART".
/// </summary>
IArt = 0x54_52_41_49
Artist = 0x54_52_41_49
}
/// <summary>
/// in "Fram"
/// Identifies chunks stored in an ANI frame list.
/// </summary>
internal enum AniListFrameType : uint
internal enum AniFrameChunkType : uint
{
/// <summary>
/// "icon"
/// An embedded frame resource, "icon".
/// </summary>
Icon = 0x6E_6F_63_69
}

11
src/ImageSharp/Formats/Ani/AniConfigurationModule.cs

@ -4,14 +4,21 @@
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// Registers the image encoder, decoder, and format detector for the ANI format.
/// </summary>
public sealed class AniConfigurationModule : IImageFormatConfigurationModule
{
/// <summary>
/// Initializes a new instance of the <see cref="AniConfigurationModule"/> class.
/// </summary>
public AniConfigurationModule()
{
}
/// <inheritdoc/>
public void Configure(Configuration configuration)
{
// configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder());
configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder());
configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector());
}

28
src/ImageSharp/Formats/Ani/AniConstants.cs

@ -3,25 +3,43 @@
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Defines constants used by the ANI format.
/// </summary>
internal static class AniConstants
{
/// <summary>
/// Gets the header bytes identifying an ani.
/// The number of bytes in the RIFF identifier, size, and form type.
/// </summary>
public const uint AniFourCc = 0x41_43_4F_4E;
public const int RiffHeaderSize = 12;
/// <summary>
/// The list of mime types that equate to an ani.
/// The number of bytes in a RIFF chunk identifier and size.
/// </summary>
public const int ChunkHeaderSize = 8;
/// <summary>
/// The number of bytes required to identify an embedded ICO or CUR resource.
/// </summary>
public const int IconDirHeaderSize = 6;
/// <summary>
/// The list of MIME types that identify ANI data.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = ["application/x-navi-animation"];
/// <summary>
/// The list of file extensions that equate to an ani.
/// The list of file extensions that identify ANI data.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["ani"];
/// <summary>
/// Gets the header bytes identifying an ani.
/// Gets the RIFF container identifier.
/// </summary>
public static ReadOnlySpan<byte> RiffFourCc => "RIFF"u8;
/// <summary>
/// Gets the ANI RIFF form type.
/// </summary>
public static ReadOnlySpan<byte> AniFormTypeFourCc => "ACON"u8;
}

19
src/ImageSharp/Formats/Ani/AniDecoder.cs

@ -1,14 +1,18 @@
// 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.
/// Decodes Windows animated cursor images.
/// </summary>
public sealed class AniDecoder : ImageDecoder
{
/// <summary>
/// Prevents a default instance of the <see cref="AniDecoder"/> class from being created.
/// </summary>
private AniDecoder()
{
}
@ -23,19 +27,26 @@ public sealed class AniDecoder : ImageDecoder
{
Guard.NotNull(options, nameof(options));
Guard.NotNull(stream, nameof(stream));
Image<TPixel> image = new AniDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken);
using AniDecoderCore decoder = new(options);
Image<TPixel> image = decoder.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);
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);
using AniDecoderCore decoder = new(options);
return decoder.Identify(options.Configuration, stream, cancellationToken);
}
}

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

File diff suppressed because it is too large

24
src/ImageSharp/Formats/Ani/AniEncoder.cs

@ -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);
}
}

468
src/ImageSharp/Formats/Ani/AniEncoderCore.cs

@ -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));
}
}
}

9
src/ImageSharp/Formats/Ani/AniFormat.cs

@ -4,10 +4,17 @@
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the bmp format.
/// Describes the ANI image format.
/// </summary>
public sealed class AniFormat : IImageFormat<AniMetadata, AniFrameMetadata>
{
/// <summary>
/// Prevents a default instance of the <see cref="AniFormat"/> class from being created.
/// </summary>
private AniFormat()
{
}
/// <summary>
/// Gets the shared instance.
/// </summary>

12
src/ImageSharp/Formats/Ani/AniFrameFormat.cs

@ -6,20 +6,20 @@ namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Specifies the format of the frame data.
/// </summary>
public enum AniFrameFormat
public enum AniFrameFormat : byte
{
/// <summary>
/// The frame data is in ICO format.
/// The frame resource is encoded as a Windows cursor.
/// </summary>
Ico = 1,
Cur,
/// <summary>
/// The frame data is in CUR format.
/// The frame resource is encoded as a Windows icon.
/// </summary>
Cur,
Ico,
/// <summary>
/// The frame data is in BMP format.
/// The frame resource is encoded as a Windows bitmap.
/// </summary>
Bmp
}

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

@ -2,14 +2,14 @@
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides Ani specific metadata information for the image.
/// Provides ANI-specific metadata for an image frame.
/// </summary>
public class AniFrameMetadata : IFormatFrameMetadata<AniFrameMetadata>
{
@ -21,77 +21,228 @@ public class AniFrameMetadata : IFormatFrameMetadata<AniFrameMetadata>
}
/// <summary>
/// Gets or sets the display time for this frame (in 1/60 seconds)
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private AniFrameMetadata(AniFrameMetadata other)
{
this.FrameDelay = other.FrameDelay;
this.SequenceNumber = other.SequenceNumber;
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.FrameFormat = other.FrameFormat;
this.Compression = other.Compression;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
this.HotspotX = other.HotspotX;
this.HotspotY = other.HotspotY;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
}
/// <summary>
/// Gets or sets the frame display time in sixtieths of a second.
/// </summary>
public uint FrameDelay { get; set; }
/// <summary>
/// Gets or sets the sequence number of current frame.
/// Gets or sets the animation sequence number.
/// Adjacent frames with the same positive value are grouped as resolution variants in one ANI frame resource.
/// A non-positive value encodes the frame as its own animation step.
/// </summary>
public int SequenceNumber { get; set; } = 1;
public int SequenceNumber { get; set; }
/// <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.
/// Gets or sets the encoded frame width.
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
/// </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.
/// Gets or sets the encoded frame height.
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
/// </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.
/// Gets or sets the format used for this frame resource.
/// </summary>
public AniFrameFormat FrameFormat { get; set; }
/// <summary>
/// Gets or sets the <see cref="IcoFrameMetadata"/> of one "icon" chunk.
/// Gets or sets the embedded ICO or CUR compression format.
/// </summary>
public IcoFrameMetadata? IcoFrameMetadata { get; set; }
public IconFrameCompression Compression { get; set; } = IconFrameCompression.Png;
/// <summary>
/// Gets or sets the <see cref="CurFrameMetadata"/> of one "icon" chunk.
/// Gets or sets the embedded bitmap bits per pixel.
/// </summary>
public CurFrameMetadata? CurFrameMetadata { get; set; }
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the embedded bitmap color table.
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }
/// <summary>
/// Gets or sets the horizontal cursor hotspot in pixels from the left.
/// </summary>
public ushort HotspotX { get; set; }
/// <summary>
/// Gets or sets the vertical cursor hotspot in pixels from the top.
/// </summary>
public ushort HotspotY { get; set; }
/// <inheritdoc/>
public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) =>
new()
public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata)
{
int bitsPerPixel = metadata.PixelTypeInfo?.BitsPerPixel ?? 32;
BmpBitsPerPixel bmpBitsPerPixel = bitsPerPixel switch
{
FrameDelay = (uint)metadata.Duration.TotalSeconds * 60
1 => BmpBitsPerPixel.Bit1,
2 => BmpBitsPerPixel.Bit2,
<= 4 => BmpBitsPerPixel.Bit4,
<= 8 => BmpBitsPerPixel.Bit8,
<= 16 => BmpBitsPerPixel.Bit16,
<= 24 => BmpBitsPerPixel.Bit24,
_ => BmpBitsPerPixel.Bit32
};
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
return new AniFrameMetadata
{
FrameDelay = (uint)Math.Round(metadata.Duration.TotalSeconds * 60),
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
Compression = bmpBitsPerPixel is BmpBitsPerPixel.Bit32 ? IconFrameCompression.Png : IconFrameCompression.Bmp,
BmpBitsPerPixel = bmpBitsPerPixel
};
}
/// <inheritdoc/>
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata()
{
// TODO: Implement. You need to consider encoding width/height.
return new() { Duration = TimeSpan.FromSeconds(this.FrameDelay / 60d) };
}
=> new()
{
Duration = TimeSpan.FromSeconds(this.FrameDelay / 60D),
EncodingWidth = this.EncodingWidth,
EncodingHeight = this.EncodingHeight,
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <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.
float ratioX = destination.Width / (float)source.Width;
float ratioY = destination.Height / (float)source.Height;
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX);
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY);
this.ColorTable = null;
}
/// <inheritdoc/>
public AniFrameMetadata DeepClone() => new()
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <inheritdoc/>
public AniFrameMetadata DeepClone() => new(this);
/// <summary>
/// Gets the pixel layout represented by the embedded resource metadata.
/// </summary>
/// <returns>The represented pixel layout.</returns>
private PixelTypeInfo GetPixelTypeInfo()
{
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
};
int bitsPerPixel = (int)this.BmpBitsPerPixel;
PixelComponentInfo componentInfo;
PixelColorType colorType;
PixelAlphaRepresentation alphaRepresentation = PixelAlphaRepresentation.None;
if (this.Compression is IconFrameCompression.Png)
{
bitsPerPixel = 32;
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8);
colorType = PixelColorType.RGB | PixelColorType.Alpha;
alphaRepresentation = PixelAlphaRepresentation.Unassociated;
}
else
{
switch (this.BmpBitsPerPixel)
{
case BmpBitsPerPixel.Bit1:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 1);
colorType = PixelColorType.Binary;
break;
case BmpBitsPerPixel.Bit2:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 2);
colorType = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit4:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 4);
colorType = PixelColorType.Indexed;
break;
case BmpBitsPerPixel.Bit8:
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 8);
colorType = PixelColorType.Indexed;
break;
// Windows bitmaps commonly use a 5-6-5 layout for 16-bit color.
case BmpBitsPerPixel.Bit16:
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 5, 6, 5);
colorType = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit24:
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 8, 8, 8);
colorType = PixelColorType.RGB;
break;
case BmpBitsPerPixel.Bit32 or _:
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8);
colorType = PixelColorType.RGB | PixelColorType.Alpha;
alphaRepresentation = PixelAlphaRepresentation.Unassociated;
break;
}
}
return new PixelTypeInfo(bitsPerPixel)
{
AlphaRepresentation = alphaRepresentation,
ComponentInfo = componentInfo,
ColorType = colorType
};
}
/// <summary>
/// Scales an encoded dimension after an image transform.
/// </summary>
/// <param name="value">The encoded source dimension.</param>
/// <param name="destination">The full destination dimension.</param>
/// <param name="ratio">The destination-to-source scale ratio.</param>
/// <returns>The encoded destination dimension.</returns>
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
{
return ClampEncodingDimension(destination);
}
// ICO and CUR encode dimensions in one byte, where zero represents 256 pixels or greater.
int source = value.Value is 0 ? 256 : value.Value;
return ClampEncodingDimension(MathF.Ceiling(source * ratio));
}
/// <summary>
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
/// </summary>
/// <param name="dimension">The pixel dimension.</param>
/// <returns>The encoded dimension.</returns>
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{
> 255 => 0,
>= 1 => (byte)dimension,
_ => 0
};
}

114
src/ImageSharp/Formats/Ani/AniFrameStream.cs

@ -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();
}

94
src/ImageSharp/Formats/Ani/AniHeader.cs

@ -1,32 +1,98 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Buffers.Binary;
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);
}
}

6
src/ImageSharp/Formats/Ani/AniHeaderFlags.cs

@ -10,12 +10,12 @@ namespace SixLabors.ImageSharp.Formats.Ani;
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.
/// The "icon" chunks contain ICO or CUR resources. Without this flag, they contain BMP resources.
/// </summary>
IsIcon = 1,
/// <summary>
/// If set, the ANI file contains a "seq " chunk.
/// The ANI file contains a "seq " chunk that maps animation steps to frame resources.
/// </summary>
ContainsSeq = 2
ContainsSequence = 2
}

27
src/ImageSharp/Formats/Ani/AniImageFormatDetector.cs

@ -2,17 +2,23 @@
// 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.
/// Detects ANI file headers.
/// </summary>
public class AniImageFormatDetector : IImageFormatDetector
public sealed class AniImageFormatDetector : IImageFormatDetector
{
/// <summary>
/// Initializes a new instance of the <see cref="AniImageFormatDetector"/> class.
/// </summary>
public AniImageFormatDetector()
{
}
/// <inheritdoc/>
public int HeaderSize => RiffOrListChunkHeader.HeaderSize;
public int HeaderSize => AniConstants.RiffHeaderSize;
/// <inheritdoc/>
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format)
@ -21,10 +27,13 @@ public class AniImageFormatDetector : IImageFormatDetector
return format is not null;
}
/// <summary>
/// Determines whether the supplied header is a RIFF container with the ANI "ACON" form type.
/// </summary>
/// <param name="header">The candidate file header.</param>
/// <returns><see langword="true"/> when the header identifies ANI data.</returns>
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header)
=> header.Length >= this.HeaderSize && RiffOrListChunkHeader.Parse(header) is
{
IsRiff: true,
FormType: AniConstants.AniFourCc
};
=> header.Length >= this.HeaderSize
&& header[..4].SequenceEqual(AniConstants.RiffFourCc)
&& header.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc);
}

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

@ -7,7 +7,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ani;
/// <summary>
/// Provides Ani specific metadata information for the image.
/// Provides ANI-specific metadata for an image.
/// </summary>
public class AniMetadata : IFormatMetadata<AniMetadata>
{
@ -19,90 +19,116 @@ public class AniMetadata : IFormatMetadata<AniMetadata>
}
/// <summary>
/// Gets or sets the width of frames in the animation.
/// Initializes a new instance of the <see cref="AniMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private AniMetadata(AniMetadata other)
{
this.Width = other.Width;
this.Height = other.Height;
this.BitCount = other.BitCount;
this.Planes = other.Planes;
this.DisplayRate = other.DisplayRate;
this.Flags = other.Flags;
this.Name = other.Name;
this.Artist = other.Artist;
}
/// <summary>
/// Gets or sets the frame width declared by the ANI header.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
/// </remarks>
public uint Width { get; set; }
/// <summary>
/// Gets or sets the height of frames in the animation.
/// Gets or sets the frame height declared by the ANI header.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
/// </remarks>
public uint Height { get; set; }
/// <summary>
/// Gets or sets the number of bits per pixel.
/// Gets or sets the bits per pixel declared by the ANI header.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// Bitmap-based ANI files use this value to describe their raw frame data. Icon-based files commonly store zero
/// because each embedded ICO or CUR entry declares its own pixel layout.
/// </remarks>
public uint BitCount { get; set; }
/// <summary>
/// Gets or sets the number of frames in the animation.
/// Gets or sets the color-plane count declared by the ANI header.
/// </summary>
/// <remarks>
/// Remains zero when <see cref="Flags"/> has flag <see cref="AniHeaderFlags.IsIcon"/>
/// Bitmap-based ANI files use one plane. Icon-based files commonly store zero because this header field is reserved
/// when the embedded resources are ICO or CUR data.
/// </remarks>
public uint Planes { get; set; }
/// <summary>
/// Gets or sets the default display rate of frames in the animation.
/// Gets or sets the default frame display rate in sixtieths of a second.
/// </summary>
public uint DisplayRate { get; set; }
/// <summary>
/// Gets or sets the flags for the ANI header.
/// Gets or sets the ANI header flags.
/// </summary>
public AniHeaderFlags Flags { get; set; }
public AniHeaderFlags Flags { get; set; } = AniHeaderFlags.IsIcon;
/// <summary>
/// Gets or sets the name of the ANI file.
/// Gets or sets the animation name.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets or sets the artist of the ANI file.
/// Gets or sets the animation artist.
/// </summary>
public string? Artist { get; set; }
/// <inheritdoc/>
public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata)
=> throw new NotImplementedException();
=> new()
{
BitCount = (uint)metadata.PixelTypeInfo.BitsPerPixel,
Planes = 1,
Flags = AniHeaderFlags.IsIcon
};
/// <inheritdoc/>
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
public PixelTypeInfo GetPixelTypeInfo()
{
// Icon-based files are allowed to leave the global bit depth unspecified. Their embedded
// ICO/CUR metadata carries the exact value, while 32-bit is the least lossy conversion default.
int bitsPerPixel = this.BitCount is > 0 and <= 32 ? (int)this.BitCount : 32;
return new PixelTypeInfo(bitsPerPixel);
}
/// <inheritdoc/>
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> new()
{
AnimateRootFrame = true,
EncodingType = EncodingType.Lossless,
PixelTypeInfo = this.GetPixelTypeInfo()
};
/// <inheritdoc/>
public PixelTypeInfo GetPixelTypeInfo()
=> throw new NotImplementedException();
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix)
where TPixel : unmanaged, IPixel<TPixel>
{
if (!this.Flags.HasFlag(AniHeaderFlags.IsIcon))
{
this.Width = (uint)destination.Width;
this.Height = (uint)destination.Height;
}
}
/// <inheritdoc/>
public FormatConnectingMetadata ToFormatConnectingMetadata()
=> throw new NotImplementedException();
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone();
/// <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
};
public AniMetadata DeepClone() => new(this);
}

34
src/ImageSharp/Formats/Ani/AniRiffChunkHeader.cs

@ -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..])
};
}

70
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -138,17 +138,30 @@ internal sealed class BmpEncoderCore
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
// Stream may not at 0.
this.Encode(image.Frames.RootFrame, image.Metadata, stream, cancellationToken);
}
/// <summary>
/// Encodes a source frame using the supplied image metadata.
/// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="frame">The source frame.</param>
/// <param name="metadata">The source image metadata.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="cancellationToken">The token to request cancellation.</param>
internal void Encode<TPixel>(ImageFrame<TPixel> frame, ImageMetadata metadata, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
// Nested ANI/ICO/CUR encoding starts inside a parent stream, so all later profile offsets use this local base.
long basePosition = stream.Position;
Configuration configuration = image.Configuration;
ImageMetadata metadata = image.Metadata;
Configuration configuration = frame.Configuration;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
ushort bpp = (ushort)this.bitsPerPixel;
int bytesPerLine = (int)(4 * ((((uint)image.Width * bpp) + 31) / 32));
this.padding = bytesPerLine - (int)(image.Width * (bpp / 8F));
int bytesPerLine = (int)(4 * ((((uint)frame.Width * bpp) + 31) / 32));
this.padding = bytesPerLine - (int)(frame.Width * (bpp / 8F));
int colorPaletteSize = this.bitsPerPixel switch
{
@ -176,25 +189,25 @@ internal sealed class BmpEncoderCore
_ => BmpInfoHeader.SizeV3
};
// for ico/cur encoder.
int height = image.Height;
// ICO/CUR DIB headers include the XOR bitmap and following AND mask in one doubled height.
int height = frame.Height;
if (this.isDoubleHeight)
{
height <<= 1;
}
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(image.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
BmpInfoHeader infoHeader = this.CreateBmpInfoHeader(frame.Width, height, infoHeaderSize, bpp, bytesPerLine, metadata, iccProfileData);
Span<byte> buffer = stackalloc byte[infoHeaderSize];
// For ico/cur encoder.
// ICO/CUR resources contain a DIB directly; standalone BMP files additionally require BITMAPFILEHEADER.
if (!this.skipFileHeader)
{
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
}
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(configuration, stream, image, cancellationToken);
this.WriteImage(configuration, stream, frame, cancellationToken);
WriteColorProfile(stream, iccProfileData, buffer, basePosition);
stream.Flush();
@ -348,15 +361,11 @@ internal sealed class BmpEncoderCore
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image">
/// <param name="frame">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
private void WriteImage<TPixel>(
Configuration configuration,
Stream stream,
Image<TPixel> image,
CancellationToken cancellationToken)
private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> frame, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrame<TPixel>? clonedFrame = null;
@ -367,11 +376,11 @@ internal sealed class BmpEncoderCore
int bpp = this.bitsPerPixel != null ? (int)this.bitsPerPixel : 32;
if (bpp > 8 && EncodingUtilities.ShouldReplaceTransparentPixels<TPixel>(this.transparentColorMode))
{
clonedFrame = image.Frames.RootFrame.Clone();
clonedFrame = frame.Clone();
EncodingUtilities.ReplaceTransparentPixels(clonedFrame);
}
ImageFrame<TPixel> encodingFrame = clonedFrame ?? image.Frames.RootFrame;
ImageFrame<TPixel> encodingFrame = clonedFrame ?? frame;
Buffer2D<TPixel> pixels = encodingFrame.PixelBuffer;
switch (this.bitsPerPixel)
@ -864,9 +873,16 @@ internal sealed class BmpEncoderCore
stream.WriteByte(indices);
}
/// <summary>
/// Writes the bottom-up 1-bit transparency mask required by an ICO/CUR bitmap resource.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="stream">The destination stream.</param>
/// <param name="encodingFrame">The source frame.</param>
private static void ProcessedAlphaMask<TPixel>(Stream stream, ImageFrame<TPixel> encodingFrame)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
// Each byte represents eight pixels and every scanline is padded to a 4-byte DIB boundary.
int arrayWidth = encodingFrame.Width / 8;
int padding = arrayWidth % 4;
if (padding is not 0)
@ -875,6 +891,9 @@ internal sealed class BmpEncoderCore
}
Span<byte> mask = stackalloc byte[arrayWidth];
Span<byte> paddingBytes = stackalloc byte[3];
paddingBytes.Clear();
for (int y = encodingFrame.Height - 1; y >= 0; y--)
{
mask.Clear();
@ -891,12 +910,21 @@ internal sealed class BmpEncoderCore
}
stream.Write(mask);
stream.Skip(padding);
// Alpha-mask rows are DWORD-aligned, and the final row padding must extend the stream.
stream.Write(paddingBytes[..padding]);
}
}
/// <summary>
/// Sets one most-significant-bit-first transparency flag in an ICO/CUR AND-mask byte.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <param name="pixel">The source pixel.</param>
/// <param name="mask">The destination mask byte.</param>
/// <param name="index">The pixel index within the byte.</param>
private static void WriteAlphaMask<TPixel>(in TPixel pixel, ref byte mask, in int index)
where TPixel : unmanaged, IPixel<TPixel>
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = pixel.ToRgba32();
if (rgba.A is 0)

6
src/ImageSharp/Formats/Cur/CurConfigurationModule.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// Registers the image encoder, decoder, and format detector for the CUR format.
/// </summary>
public sealed class CurConfigurationModule : IImageFormatConfigurationModule
{
@ -15,6 +13,6 @@ public sealed class CurConfigurationModule : IImageFormatConfigurationModule
{
configuration.ImageFormatsManager.SetEncoder(CurFormat.Instance, new CurEncoder());
configuration.ImageFormatsManager.SetDecoder(CurFormat.Instance, CurDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
configuration.ImageFormatsManager.AddImageFormatDetector(new CurImageFormatDetector());
}
}

10
src/ImageSharp/Formats/Cur/CurConstants.cs

@ -4,12 +4,12 @@
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Defines constants relating to ICOs
/// Defines constants used by the CUR format.
/// </summary>
internal static class CurConstants
{
/// <summary>
/// The list of mime types that equate to a cur.
/// The MIME types that identify CUR data.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
@ -27,13 +27,11 @@ internal static class CurConstants
"image/ico",
"image/icon",
"text/ico",
"application/ico",
"application/ico"
];
/// <summary>
/// The list of file extensions that equate to a cur.
/// The file extensions that identify CUR data.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["cur"];
public const uint FileHeader = 0x00_02_00_00;
}

7
src/ImageSharp/Formats/Cur/CurDecoder.cs

@ -6,10 +6,13 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Decoder for generating an image out of a ico encoded stream.
/// Decoder for generating an image from a CUR encoded stream.
/// </summary>
public sealed class CurDecoder : ImageDecoder
{
/// <summary>
/// Initializes a new instance of the <see cref="CurDecoder"/> class.
/// </summary>
private CurDecoder()
{
}
@ -34,7 +37,7 @@ public sealed class CurDecoder : ImageDecoder
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

10
src/ImageSharp/Formats/Cur/CurDecoderCore.cs

@ -7,13 +7,21 @@ using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Decodes CUR containers and maps directory metadata to CUR metadata.
/// </summary>
internal sealed class CurDecoderCore : IconDecoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="CurDecoderCore"/> class.
/// </summary>
/// <param name="options">The decoder options.</param>
public CurDecoderCore(DecoderOptions options)
: base(options)
: base(options, IconFileType.CUR)
{
}
/// <inheritdoc/>
protected override void SetFrameMetadata(
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,

5
src/ImageSharp/Formats/Cur/CurEncoder.cs

@ -10,8 +10,5 @@ public sealed class CurEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
CurEncoderCore encoderCore = new(this);
encoderCore.Encode(image, stream, cancellationToken);
}
=> new CurEncoderCore(this).Encode(image, stream, cancellationToken);
}

33
src/ImageSharp/Formats/Cur/CurEncoderCore.cs

@ -2,13 +2,46 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Encodes CUR containers using CUR frame metadata.
/// </summary>
internal sealed class CurEncoderCore : IconEncoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="CurEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder options.</param>
public CurEncoderCore(QuantizingImageEncoder encoder)
: base(encoder, IconFileType.CUR)
{
}
/// <summary>
/// Encodes all source frames as a CUR resource.
/// </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>
=> this.Encode(image, stream, default(CurFrameMetadataProvider), cancellationToken);
/// <summary>
/// Supplies CUR frame metadata to the shared container encoder.
/// </summary>
private readonly struct CurFrameMetadataProvider : IEncodingFrameMetadataProvider
{
/// <inheritdoc/>
public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable)
{
CurFrameMetadata metadata = frame.Metadata.GetCurMetadata();
colorTable = metadata.ColorTable;
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry(frame.Size));
}
}
}

9
src/ImageSharp/Formats/Cur/CurFormat.cs

@ -4,10 +4,13 @@
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the ICO format.
/// Describes the CUR image format.
/// </summary>
public sealed class CurFormat : IImageFormat<CurMetadata, CurFrameMetadata>
{
/// <summary>
/// Prevents a default instance of the <see cref="CurFormat"/> class from being created.
/// </summary>
private CurFormat()
{
}
@ -18,10 +21,10 @@ public sealed class CurFormat : IImageFormat<CurMetadata, CurFrameMetadata>
public static CurFormat Instance { get; } = new();
/// <inheritdoc/>
public string Name => "ICO";
public string Name => "CUR";
/// <inheritdoc/>
public string DefaultMimeType => CurConstants.MimeTypes.First();
public string DefaultMimeType => "image/vnd.microsoft.icon";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => CurConstants.MimeTypes;

52
src/ImageSharp/Formats/Cur/CurFrameMetadata.cs

@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// IcoFrameMetadata.
/// Provides CUR-specific metadata for an image frame.
/// </summary>
public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
@ -20,6 +20,10 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CurFrameMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private CurFrameMetadata(CurFrameMetadata other)
{
this.Compression = other.Compression;
@ -28,10 +32,15 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
this.EncodingWidth = other.EncodingWidth;
this.EncodingHeight = other.EncodingHeight;
this.BmpBitsPerPixel = other.BmpBitsPerPixel;
if (other.ColorTable?.Length > 0)
{
this.ColorTable = other.ColorTable.Value.ToArray();
}
}
/// <summary>
/// Gets or sets the frame compressions format.
/// Gets or sets the frame compression format.
/// </summary>
public IconFrameCompression Compression { get; set; }
@ -46,14 +55,14 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
public ushort HotspotY { get; set; }
/// <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.
/// Gets or sets the encoded width.
/// A value of zero represents 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.
/// Gets or sets the encoded height.
/// A value of zero represents 256 pixels or greater.
/// </summary>
public byte? EncodingHeight { get; set; }
@ -104,7 +113,7 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
BmpBitsPerPixel = bbpp,
Compression = compression,
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight),
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight)
};
}
@ -134,6 +143,10 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
/// <inheritdoc/>
public CurFrameMetadata DeepClone() => new(this);
/// <summary>
/// Copies the observable CUR directory values from an entry.
/// </summary>
/// <param name="entry">The source directory entry.</param>
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
@ -142,6 +155,11 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
this.HotspotY = entry.BitCount;
}
/// <summary>
/// Creates a CUR directory entry from this metadata.
/// </summary>
/// <param name="size">The source frame size.</param>
/// <returns>The CUR directory entry.</returns>
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
@ -158,6 +176,10 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
/// <summary>
/// Gets the pixel layout represented by this metadata.
/// </summary>
/// <returns>The represented pixel layout.</returns>
private PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
@ -219,6 +241,13 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
};
}
/// <summary>
/// Scales an encoded dimension after an image transform.
/// </summary>
/// <param name="value">The encoded source dimension.</param>
/// <param name="destination">The full destination dimension.</param>
/// <param name="ratio">The destination-to-source scale ratio.</param>
/// <returns>The encoded destination dimension.</returns>
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
@ -226,9 +255,16 @@ public class CurFrameMetadata : IFormatFrameMetadata<CurFrameMetadata>
return ClampEncodingDimension(destination);
}
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
// A stored zero represents 256 pixels, so scaling must expand it before applying the transform ratio.
int source = value.Value is 0 ? 256 : value.Value;
return ClampEncodingDimension(MathF.Ceiling(source * ratio));
}
/// <summary>
/// Converts a pixel dimension to the one-byte CUR representation.
/// </summary>
/// <param name="dimension">The pixel dimension.</param>
/// <returns>The encoded dimension.</returns>
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{

49
src/ImageSharp/Formats/Cur/CurImageFormatDetector.cs

@ -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);
}
}

6
src/ImageSharp/Formats/Cur/CurMetadata.cs

@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Cur;
/// <summary>
/// Provides Cur specific metadata information for the image.
/// Provides CUR-specific metadata for an image.
/// </summary>
public class CurMetadata : IFormatMetadata<CurMetadata>
{
@ -32,7 +32,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
}
/// <summary>
/// Gets or sets the frame compressions format. Derived from the root frame.
/// Gets or sets the root frame compression format.
/// </summary>
public IconFrameCompression Compression { get; set; }
@ -43,7 +43,7 @@ public class CurMetadata : IFormatMetadata<CurMetadata>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// Gets or sets the root frame color table, if any.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }

32
src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs

@ -15,9 +15,10 @@ internal class B44ExrCompression : ExrBaseDecompressor
{
private readonly int channelCount;
private readonly byte[] scratch = new byte[14];
// B44 encodes each 4x4 block in either 3 or 14 bytes, so both representations share this inline storage.
private InlineArray14<byte> scratch;
private readonly ushort[] s = new ushort[16];
private InlineArray16<ushort> s;
private readonly IMemoryOwner<ushort> tmpBuffer;
@ -42,8 +43,11 @@ internal class B44ExrCompression : ExrBaseDecompressor
{
Span<ushort> outputBuffer = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<ushort> decompressed = this.tmpBuffer.GetSpan();
Span<byte> scratch = this.scratch;
Span<ushort> samples = this.s;
int outputOffset = 0;
int bytesLeft = (int)compressedBytes;
for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
{
for (int y = 0; y < this.RowsPerBlock; y += 4)
@ -60,49 +64,49 @@ internal class B44ExrCompression : ExrBaseDecompressor
int rowOffset = 0;
for (int x = 0; x < this.Width && bytesLeft > 0; x += 4)
{
int bytesRead = stream.Read(this.scratch, 0, 3);
int bytesRead = stream.Read(scratch[..3]);
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!");
}
// Check if 3-byte encoded flat field.
if (this.scratch[2] >= 13 << 2)
if (scratch[2] >= 13 << 2)
{
Unpack3(this.scratch, this.s);
Unpack3(scratch, samples);
bytesLeft -= 3;
}
else
{
bytesRead = stream.Read(this.scratch, 3, 11);
bytesRead = stream.Read(scratch.Slice(3, 11));
if (bytesRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!");
}
Unpack14(this.scratch, this.s);
Unpack14(scratch, samples);
bytesLeft -= 14;
}
int n = x + 3 < this.Width ? 4 : this.Width - x;
if (y + 3 < this.RowsPerBlock)
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
this.s.AsSpan(12, n).CopyTo(row3[rowOffset..]);
samples[..n].CopyTo(row0[rowOffset..]);
samples.Slice(4, n).CopyTo(row1[rowOffset..]);
samples.Slice(8, n).CopyTo(row2[rowOffset..]);
samples.Slice(12, n).CopyTo(row3[rowOffset..]);
}
else
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
samples[..n].CopyTo(row0[rowOffset..]);
if (y + 1 < this.RowsPerBlock)
{
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
samples.Slice(4, n).CopyTo(row1[rowOffset..]);
}
if (y + 2 < this.RowsPerBlock)
{
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
samples.Slice(8, n).CopyTo(row2[rowOffset..]);
}
}

2
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -24,7 +24,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[8];
private InlineArray8<byte> buffer;
/// <summary>
/// Used for allocating memory during processing operations.

32
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -21,7 +21,7 @@ internal sealed class ExrEncoderCore
/// <summary>
/// Reusable buffer.
/// </summary>
private readonly byte[] buffer = new byte[8];
private InlineArray8<byte> buffer;
/// <summary>
/// Used for allocating memory during processing operations.
@ -108,7 +108,7 @@ internal sealed class ExrEncoderCore
// Write magick bytes.
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
// Version number.
this.buffer[0] = 2;
@ -117,7 +117,7 @@ internal sealed class ExrEncoderCore
this.buffer[1] = 0;
this.buffer[2] = 0;
this.buffer[3] = 0;
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
// Write EXR header.
this.WriteHeader(stream, header);
@ -194,7 +194,7 @@ internal sealed class ExrEncoderCore
// Write row index.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
// At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
long pixelDataSizePos = stream.Position;
@ -237,7 +237,7 @@ internal sealed class ExrEncoderCore
// Write pixel row data size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
stream.Position = pixelDataSizePos;
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
stream.Position = positionAfterPixelData;
cancellationToken.ThrowIfCancellationRequested();
@ -293,7 +293,7 @@ internal sealed class ExrEncoderCore
// Write row index.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
// At this point, it is not yet known how much bytes the compressed data will take up, keep stream position.
long pixelDataSizePos = stream.Position;
@ -328,7 +328,7 @@ internal sealed class ExrEncoderCore
// Write pixel row data size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes);
stream.Position = pixelDataSizePos;
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
stream.Position = positionAfterPixelData;
cancellationToken.ThrowIfCancellationRequested();
@ -518,7 +518,7 @@ internal sealed class ExrEncoderCore
WriteString(stream, channelInfo.ChannelName);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
stream.WriteByte(channelInfo.Linear);
@ -528,10 +528,10 @@ internal sealed class ExrEncoderCore
stream.WriteByte(0);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
}
/// <summary>
@ -629,7 +629,7 @@ internal sealed class ExrEncoderCore
// Write attribute size.
BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
}
/// <summary>
@ -656,16 +656,16 @@ internal sealed class ExrEncoderCore
private void WriteBoxInteger(Stream stream, ExrBox2i box)
{
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
}
/// <summary>
@ -677,7 +677,7 @@ internal sealed class ExrEncoderCore
private unsafe void WriteSingle(Stream stream, float value)
{
BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value);
stream.Write(this.buffer.AsSpan(0, 4));
stream.Write(this.buffer[..4]);
}
/// <summary>

30
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -22,7 +22,7 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <summary>
/// The temp buffer used to reduce allocations.
/// </summary>
private ScratchBuffer buffer; // mutable struct, don't make readonly
private InlineArray16<byte> buffer; // mutable struct, don't make readonly
/// <summary>
/// The global color table.
@ -285,13 +285,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadGraphicalControlExtension(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, 6);
int bytesRead = stream.Read(this.buffer, 0, 6);
if (bytesRead != 6)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
}
this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer.Span);
this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer);
}
/// <summary>
@ -300,13 +300,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadImageDescriptor(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, 9);
int bytesRead = stream.Read(this.buffer, 0, 9);
if (bytesRead != 9)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
}
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer.Span);
this.imageDescriptor = GifImageDescriptor.Parse(this.buffer);
if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0)
{
GifThrowHelper.ThrowInvalidImageContentException("Width or height should not be 0");
@ -321,13 +321,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, 7);
int bytesRead = stream.Read(this.buffer, 0, 7);
if (bytesRead != 7)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
}
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer.Span);
this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer);
}
/// <summary>
@ -353,13 +353,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = stream.Position;
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.ApplicationBlockSize);
int bytesRead = stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
if (bytesRead != GifConstants.ApplicationBlockSize)
{
GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
bool isXmp = this.buffer.Span.StartsWith(GifConstants.XmpApplicationIdentificationBytes);
bool isXmp = ((ReadOnlySpan<byte>)this.buffer).StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp)
{
this.ReadXmpApplicationExtension(stream, position, appLength);
@ -447,13 +447,13 @@ internal sealed class GifDecoderCore : ImageDecoderCore
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadNetscapeApplicationExtensionData(BufferedReadStream stream)
{
int bytesRead = stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize);
int bytesRead = stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
if (bytesRead != GifConstants.NetscapeLoopingSubBlockSize)
{
throw new InvalidImageContentException("Unexpected end of stream while reading gif application extension");
}
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount;
this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer[1..]).RepeatCount;
int terminator = stream.ReadByte();
if (terminator == -1)
@ -996,12 +996,4 @@ internal sealed class GifDecoderCore : ImageDecoderCore
this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index];
}
}
private unsafe struct ScratchBuffer
{
private const int Size = 16;
private fixed byte scratch[Size];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
}

6
src/ImageSharp/Formats/Ico/IcoConfigurationModule.cs

@ -1,12 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Registers the image encoders, decoders and mime type detectors for the Ico format.
/// Registers the image encoder, decoder, and format detector for the ICO format.
/// </summary>
public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
{
@ -15,6 +13,6 @@ public sealed class IcoConfigurationModule : IImageFormatConfigurationModule
{
configuration.ImageFormatsManager.SetEncoder(IcoFormat.Instance, new IcoEncoder());
configuration.ImageFormatsManager.SetDecoder(IcoFormat.Instance, IcoDecoder.Instance);
configuration.ImageFormatsManager.AddImageFormatDetector(new IconImageFormatDetector());
configuration.ImageFormatsManager.AddImageFormatDetector(new IcoImageFormatDetector());
}
}

10
src/ImageSharp/Formats/Ico/IcoConstants.cs

@ -4,12 +4,12 @@
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Defines constants relating to ICOs
/// Defines constants used by the ICO format.
/// </summary>
internal static class IcoConstants
{
/// <summary>
/// The list of mime types that equate to a ico.
/// The MIME types that identify ICO data.
/// </summary>
/// <remarks>
/// See <see href="https://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type"/>
@ -27,13 +27,11 @@ internal static class IcoConstants
"image/ico",
"image/icon",
"text/ico",
"application/ico",
"application/ico"
];
/// <summary>
/// The list of file extensions that equate to a ico.
/// The file extensions that identify ICO data.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = ["ico"];
public const uint FileHeader = 0x00_01_00_00;
}

7
src/ImageSharp/Formats/Ico/IcoDecoder.cs

@ -6,10 +6,13 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Decoder for generating an image out of a ico encoded stream.
/// Decoder for generating an image from an ICO encoded stream.
/// </summary>
public sealed class IcoDecoder : ImageDecoder
{
/// <summary>
/// Initializes a new instance of the <see cref="IcoDecoder"/> class.
/// </summary>
private IcoDecoder()
{
}
@ -34,7 +37,7 @@ public sealed class IcoDecoder : ImageDecoder
/// <inheritdoc/>
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken)
=> this.Decode<Rgba32>(options, stream, cancellationToken);
=> this.Decode<Rgba32>(options, stream, cancellationToken);
/// <inheritdoc/>
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken)

18
src/ImageSharp/Formats/Ico/IcoDecoderCore.cs

@ -7,13 +7,21 @@ using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Decodes ICO containers and maps directory metadata to ICO metadata.
/// </summary>
internal sealed class IcoDecoderCore : IconDecoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="IcoDecoderCore"/> class.
/// </summary>
/// <param name="options">The decoder options.</param>
public IcoDecoderCore(DecoderOptions options)
: base(options)
: base(options, IconFileType.ICO)
{
}
/// <inheritdoc/>
protected override void SetFrameMetadata(
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
@ -31,10 +39,10 @@ internal sealed class IcoDecoderCore : IconDecoderCore
if (index == 0)
{
IcoMetadata curMetadata = imageMetadata.GetIcoMetadata();
curMetadata.Compression = compression;
curMetadata.BmpBitsPerPixel = bitsPerPixel;
curMetadata.ColorTable = colorTable;
IcoMetadata icoMetadata = imageMetadata.GetIcoMetadata();
icoMetadata.Compression = compression;
icoMetadata.BmpBitsPerPixel = bitsPerPixel;
icoMetadata.ColorTable = colorTable;
}
}
}

5
src/ImageSharp/Formats/Ico/IcoEncoder.cs

@ -10,8 +10,5 @@ public sealed class IcoEncoder : QuantizingImageEncoder
{
/// <inheritdoc/>
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken)
{
IcoEncoderCore encoderCore = new(this);
encoderCore.Encode(image, stream, cancellationToken);
}
=> new IcoEncoderCore(this).Encode(image, stream, cancellationToken);
}

33
src/ImageSharp/Formats/Ico/IcoEncoderCore.cs

@ -2,13 +2,46 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Encodes ICO containers using ICO frame metadata.
/// </summary>
internal sealed class IcoEncoderCore : IconEncoderCore
{
/// <summary>
/// Initializes a new instance of the <see cref="IcoEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder options.</param>
public IcoEncoderCore(QuantizingImageEncoder encoder)
: base(encoder, IconFileType.ICO)
{
}
/// <summary>
/// Encodes all source frames as an ICO resource.
/// </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>
=> this.Encode(image, stream, default(IcoFrameMetadataProvider), cancellationToken);
/// <summary>
/// Supplies ICO frame metadata to the shared container encoder.
/// </summary>
private readonly struct IcoFrameMetadataProvider : IEncodingFrameMetadataProvider
{
/// <inheritdoc/>
public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable)
{
IcoFrameMetadata metadata = frame.Metadata.GetIcoMetadata();
colorTable = metadata.ColorTable;
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ToIconDirEntry(frame.Size));
}
}
}

5
src/ImageSharp/Formats/Ico/IcoFormat.cs

@ -8,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Ico;
/// </summary>
public sealed class IcoFormat : IImageFormat<IcoMetadata, IcoFrameMetadata>
{
/// <summary>
/// Prevents a default instance of the <see cref="IcoFormat"/> class from being created.
/// </summary>
private IcoFormat()
{
}
@ -21,7 +24,7 @@ public sealed class IcoFormat : IImageFormat<IcoMetadata, IcoFrameMetadata>
public string Name => "ICO";
/// <inheritdoc/>
public string DefaultMimeType => IcoConstants.MimeTypes.First();
public string DefaultMimeType => "image/vnd.microsoft.icon";
/// <inheritdoc/>
public IEnumerable<string> MimeTypes => IcoConstants.MimeTypes;

49
src/ImageSharp/Formats/Ico/IcoFrameMetadata.cs

@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Provides Ico specific metadata information for the image frame.
/// Provides ICO-specific metadata for an image frame.
/// </summary>
public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
@ -20,6 +20,10 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IcoFrameMetadata"/> class by copying another instance.
/// </summary>
/// <param name="other">The metadata to copy.</param>
private IcoFrameMetadata(IcoFrameMetadata other)
{
this.Compression = other.Compression;
@ -34,19 +38,19 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
}
/// <summary>
/// Gets or sets the frame compressions format.
/// Gets or sets the frame compression format.
/// </summary>
public IconFrameCompression Compression { get; set; }
/// <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.
/// Gets or sets the encoded width.
/// A value of zero represents 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.
/// Gets or sets the encoded height.
/// A value of zero represents 256 pixels or greater.
/// </summary>
public byte? EncodingHeight { get; set; }
@ -127,12 +131,21 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
/// <inheritdoc/>
public IcoFrameMetadata DeepClone() => new(this);
/// <summary>
/// Copies the observable ICO directory values from an entry.
/// </summary>
/// <param name="entry">The source directory entry.</param>
internal void FromIconDirEntry(IconDirEntry entry)
{
this.EncodingWidth = entry.Width;
this.EncodingHeight = entry.Height;
}
/// <summary>
/// Creates an ICO directory entry from this metadata.
/// </summary>
/// <param name="size">The source frame size.</param>
/// <returns>The ICO directory entry.</returns>
internal IconDirEntry ToIconDirEntry(Size size)
{
byte colorCount = this.Compression == IconFrameCompression.Png || this.BmpBitsPerPixel > BmpBitsPerPixel.Bit8
@ -148,11 +161,15 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
BitCount = this.Compression switch
{
IconFrameCompression.Bmp => (ushort)this.BmpBitsPerPixel,
IconFrameCompression.Png or _ => 32,
},
IconFrameCompression.Png or _ => 32
}
};
}
/// <summary>
/// Gets the pixel layout represented by this metadata.
/// </summary>
/// <returns>The represented pixel layout.</returns>
private PixelTypeInfo GetPixelTypeInfo()
{
int bpp = (int)this.BmpBitsPerPixel;
@ -214,6 +231,13 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
};
}
/// <summary>
/// Scales an encoded dimension after an image transform.
/// </summary>
/// <param name="value">The encoded source dimension.</param>
/// <param name="destination">The full destination dimension.</param>
/// <param name="ratio">The destination-to-source scale ratio.</param>
/// <returns>The encoded destination dimension.</returns>
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio)
{
if (value is null)
@ -221,9 +245,16 @@ public class IcoFrameMetadata : IFormatFrameMetadata<IcoFrameMetadata>
return ClampEncodingDimension(destination);
}
return ClampEncodingDimension(MathF.Ceiling(value.Value * ratio));
// A stored zero represents 256 pixels, so scaling must expand it before applying the transform ratio.
int source = value.Value is 0 ? 256 : value.Value;
return ClampEncodingDimension(MathF.Ceiling(source * ratio));
}
/// <summary>
/// Converts a pixel dimension to the one-byte ICO representation.
/// </summary>
/// <param name="dimension">The pixel dimension.</param>
/// <returns>The encoded dimension.</returns>
private static byte ClampEncodingDimension(float? dimension)
=> dimension switch
{

49
src/ImageSharp/Formats/Ico/IcoImageFormatDetector.cs

@ -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);
}
}

6
src/ImageSharp/Formats/Ico/IcoMetadata.cs

@ -9,7 +9,7 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Ico;
/// <summary>
/// Provides Ico specific metadata information for the image.
/// Provides ICO-specific metadata for an image.
/// </summary>
public class IcoMetadata : IFormatMetadata<IcoMetadata>
{
@ -32,7 +32,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
}
/// <summary>
/// Gets or sets the frame compressions format. Derived from the root frame.
/// Gets or sets the root frame compression format.
/// </summary>
public IconFrameCompression Compression { get; set; }
@ -43,7 +43,7 @@ public class IcoMetadata : IFormatMetadata<IcoMetadata>
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32;
/// <summary>
/// Gets or sets the color table, if any. Derived from the root frame.<br/>
/// Gets or sets the root frame color table, if any.<br/>
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
/// </summary>
public ReadOnlyMemory<Color>? ColorTable { get; set; }

415
src/ImageSharp/Formats/Icon/IconDecoderCore.cs

@ -9,15 +9,28 @@ using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// Decodes the shared ICO/CUR directory and embedded BMP or PNG frame payloads.
/// </summary>
internal abstract class IconDecoderCore : ImageDecoderCore
{
private readonly IconFileType iconFileType;
private IconDir fileHeader;
private IconDirEntry[]? entries;
protected IconDecoderCore(DecoderOptions options)
/// <summary>
/// Reusable storage for an icon directory entry and smaller fixed values.
/// </summary>
private InlineArray16<byte> buffer;
/// <summary>
/// Initializes a new instance of the <see cref="IconDecoderCore"/> class.
/// </summary>
/// <param name="options">The decoder options.</param>
/// <param name="iconFileType">The expected icon container type.</param>
protected IconDecoderCore(DecoderOptions options, IconFileType iconFileType)
: base(options)
{
}
=> this.iconFileType = iconFileType;
/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
@ -26,108 +39,126 @@ internal abstract class IconDecoderCore : ImageDecoderCore
long basePosition = stream.Position;
this.ReadHeader(stream);
Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length];
List<(Image<TPixel> Image, IconFrameCompression Compression, int Index)> decodedEntries
= new((int)Math.Min(this.entries.Length, this.Options.MaxFrames));
int entryCount = this.entries.Length;
(int EntryIndex, Image<TPixel> Image, IconFrameCompression Compression)[] decodedEntries = new (int, Image<TPixel>, IconFrameCompression)[entryCount];
int decodedCount = 0;
IconFrameStream frameStream = new(stream);
this.Dimensions = default;
for (int i = 0; i < this.entries.Length; i++)
try
{
if (i == this.Options.MaxFrames)
for (int i = 0; i < entryCount; i++)
{
break;
}
ref IconDirEntry entry = ref this.entries[i];
int entryIndex = i;
// If we hit the end of the stream we should break.
if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length)
{
break;
}
// There should always be enough bytes for this regardless of the entry type.
if (stream.Read(flag) != PngConstants.HeaderBytes.Length)
{
break;
}
this.ExecuteImageDataSegmentAction(() =>
{
cancellationToken.ThrowIfCancellationRequested();
// Reset the stream position.
_ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current);
ref IconDirEntry entry = ref this.entries[entryIndex];
this.SetFrameStreamBounds(frameStream, stream, basePosition, entry);
Span<byte> flag = this.buffer[..PngConstants.HeaderBytes.Length];
CheckEndOfStream(frameStream.Read(flag), flag.Length);
frameStream.Position = 0;
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
IconFrameCompression compression = isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp;
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
Image<TPixel> temp = this.GetDecoder(isPng).Decode<TPixel>(this.Options.Configuration, stream, cancellationToken);
decodedEntries.Add((temp, isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp, i));
// Frames remain alive until the largest decoded dimensions are known and the common canvas can be allocated.
Image<TPixel> decoded = this.GetDecoder(isPng).Decode<TPixel>(this.Options.Configuration, frameStream, cancellationToken);
decodedEntries[decodedCount++] = (entryIndex, decoded, compression);
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, temp.Size.Width), Math.Max(this.Dimensions.Height, temp.Size.Height));
}
// The embedded header is authoritative because a zero directory dimension can represent 256 pixels or a larger Vista-era PNG.
this.Dimensions = new(Math.Max(this.Dimensions.Width, decoded.Width), Math.Max(this.Dimensions.Height, decoded.Height));
});
}
ImageMetadata metadata = new();
BmpMetadata? bmpMetadata = null;
PngMetadata? pngMetadata = null;
Image<TPixel> result = new(this.Options.Configuration, metadata, decodedEntries.Select(x =>
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
ImageFrame<TPixel> source = x.Image.Frames.RootFrameUnsafe;
for (int y = 0; y < source.Height; y++)
if (decodedCount is 0)
{
source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y));
throw new InvalidImageContentException("The icon file does not contain any decodable image entries.");
}
// Copy the format specific frame metadata to the image.
if (x.Compression is IconFrameCompression.Png)
ImageMetadata metadata = new();
BmpMetadata? bmpMetadata = null;
PngMetadata? pngMetadata = null;
ImageFrame<TPixel>[] frames = new ImageFrame<TPixel>[decodedCount];
int initializedFrameCount = 0;
try
{
if (x.Index == 0)
for (int i = 0; i < decodedCount; i++)
{
pngMetadata = x.Image.Metadata.GetPngMetadata();
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
Image<TPixel> decoded = decodedEntries[i].Image;
ref IconDirEntry entry = ref this.entries[decodedEntries[i].EntryIndex];
ImageFrame<TPixel> source = decoded.Frames.RootFrameUnsafe;
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions);
frames[i] = target;
initializedFrameCount++;
for (int y = 0; y < source.Height; y++)
{
source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y));
}
// Preserve both the embedded format metadata and the ICO/CUR directory metadata on the output frame.
if (decodedEntries[i].Compression is IconFrameCompression.Png)
{
if (i == 0)
{
pngMetadata = decoded.Metadata.GetPngMetadata();
}
target.Metadata.SetFormatMetadata(PngFormat.Instance, source.Metadata.GetPngMetadata());
}
else
{
BmpMetadata currentBmpMetadata = decoded.Metadata.GetBmpMetadata();
bitsPerPixel = currentBmpMetadata.BitsPerPixel;
colorTable = currentBmpMetadata.ColorTable;
if (i == 0)
{
bmpMetadata = currentBmpMetadata;
}
}
this.SetFrameMetadata(metadata, target.Metadata, i, entry, decodedEntries[i].Compression, bitsPerPixel, colorTable);
}
target.Metadata.SetFormatMetadata(PngFormat.Instance, target.Metadata.GetPngMetadata());
}
else
{
BmpMetadata meta = x.Image.Metadata.GetBmpMetadata();
bitsPerPixel = meta.BitsPerPixel;
colorTable = meta.ColorTable;
if (x.Index == 0)
// Embedded metadata belongs to the container even though the temporary decoded images are disposed below.
if (bmpMetadata is not null)
{
bmpMetadata = meta;
metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
}
}
this.SetFrameMetadata(
metadata,
target.Metadata,
x.Index,
this.entries[x.Index],
x.Compression,
bitsPerPixel,
colorTable);
x.Image.Dispose();
if (pngMetadata is not null)
{
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
}
return target;
}).ToArray());
Image<TPixel> result = new(this.Options.Configuration, metadata, frames);
// Copy the format specific metadata to the image.
if (bmpMetadata != null)
{
result.Metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
// Ownership of every output frame transfers to the result only after construction succeeds.
initializedFrameCount = 0;
return result;
}
finally
{
for (int i = 0; i < initializedFrameCount; i++)
{
frames[i].Dispose();
}
}
}
if (pngMetadata != null)
finally
{
result.Metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
for (int i = 0; i < decodedCount; i++)
{
decodedEntries[i].Image.Dispose();
}
}
return result;
}
/// <inheritdoc />
@ -137,87 +168,82 @@ internal abstract class IconDecoderCore : ImageDecoderCore
long basePosition = stream.Position;
this.ReadHeader(stream);
Span<byte> flag = stackalloc byte[PngConstants.HeaderBytes.Length];
ImageMetadata metadata = new();
BmpMetadata? bmpMetadata = null;
PngMetadata? pngMetadata = null;
ImageFrameMetadata[] frames = new ImageFrameMetadata[Math.Min(this.fileHeader.Count, this.Options.MaxFrames)];
int bpp = 0;
ImageFrameMetadata[] frames = new ImageFrameMetadata[this.entries.Length];
int frameCount = 0;
IconFrameStream frameStream = new(stream);
this.Dimensions = default;
for (int i = 0; i < frames.Length; i++)
{
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ref IconDirEntry entry = ref this.entries[i];
int entryIndex = i;
// If we hit the end of the stream we should break.
if (stream.Seek(basePosition + entry.ImageOffset, SeekOrigin.Begin) >= stream.Length)
this.ExecuteImageDataSegmentAction(() =>
{
break;
}
// There should always be enough bytes for this regardless of the entry type.
if (stream.Read(flag) != PngConstants.HeaderBytes.Length)
{
break;
}
cancellationToken.ThrowIfCancellationRequested();
// Reset the stream position.
_ = stream.Seek(-PngConstants.HeaderBytes.Length, SeekOrigin.Current);
BmpBitsPerPixel bitsPerPixel = BmpBitsPerPixel.Bit32;
ReadOnlyMemory<Color>? colorTable = null;
ref IconDirEntry entry = ref this.entries[entryIndex];
this.SetFrameStreamBounds(frameStream, stream, basePosition, entry);
Span<byte> flag = this.buffer[..PngConstants.HeaderBytes.Length];
CheckEndOfStream(frameStream.Read(flag), flag.Length);
frameStream.Position = 0;
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
bool isPng = flag.SequenceEqual(PngConstants.HeaderBytes);
ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, frameStream, cancellationToken);
ImageFrameMetadata frameMetadata = new();
// Decode the frame into a temp image buffer. This is disposed after the frame is copied to the result.
ImageInfo frameInfo = this.GetDecoder(isPng).Identify(this.Options.Configuration, stream, cancellationToken);
ImageFrameMetadata frameMetadata = new();
if (isPng)
{
if (i == 0)
if (isPng)
{
pngMetadata = frameInfo.Metadata.GetPngMetadata();
}
if (frameCount is 0)
{
pngMetadata = frameInfo.Metadata.GetPngMetadata();
}
frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata());
}
else
{
BmpMetadata meta = frameInfo.Metadata.GetBmpMetadata();
bitsPerPixel = meta.BitsPerPixel;
colorTable = meta.ColorTable;
if (i == 0)
frameMetadata.SetFormatMetadata(PngFormat.Instance, frameInfo.FrameMetadataCollection[0].GetPngMetadata());
}
else
{
bmpMetadata = meta;
BmpMetadata currentBmpMetadata = frameInfo.Metadata.GetBmpMetadata();
bitsPerPixel = currentBmpMetadata.BitsPerPixel;
colorTable = currentBmpMetadata.ColorTable;
if (frameCount is 0)
{
bmpMetadata = currentBmpMetadata;
}
}
}
bpp = Math.Max(bpp, (int)bitsPerPixel);
IconFrameCompression compression = isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp;
this.SetFrameMetadata(metadata, frameMetadata, frameCount, entry, compression, bitsPerPixel, colorTable);
frames[frameCount++] = frameMetadata;
frames[i] = frameMetadata;
// Identification uses the same embedded-header dimensions as decoding, without allocating pixel buffers.
this.Dimensions = new(Math.Max(this.Dimensions.Width, frameInfo.Width), Math.Max(this.Dimensions.Height, frameInfo.Height));
});
}
this.SetFrameMetadata(
metadata,
frames[i],
i,
this.entries[i],
isPng ? IconFrameCompression.Png : IconFrameCompression.Bmp,
bitsPerPixel,
colorTable);
if (frameCount is 0)
{
throw new InvalidImageContentException("The icon file does not contain any identifiable image entries.");
}
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, frameInfo.Size.Width), Math.Max(this.Dimensions.Height, frameInfo.Size.Height));
if (frameCount != frames.Length)
{
// Preserve successfully identified frames when truncated image data ends the scan before the declared directory count.
Array.Resize(ref frames, frameCount);
}
// Copy the format specific metadata to the image.
if (bmpMetadata != null)
if (bmpMetadata is not null)
{
metadata.SetFormatMetadata(BmpFormat.Instance, bmpMetadata);
}
if (pngMetadata != null)
if (pngMetadata is not null)
{
metadata.SetFormatMetadata(PngFormat.Instance, pngMetadata);
}
@ -225,6 +251,16 @@ internal abstract class IconDecoderCore : ImageDecoderCore
return new ImageInfo(this.Dimensions, metadata, frames);
}
/// <summary>
/// Copies format-specific directory and embedded-frame metadata to an output frame.
/// </summary>
/// <param name="imageMetadata">The output image metadata.</param>
/// <param name="frameMetadata">The output frame metadata.</param>
/// <param name="index">The directory entry index.</param>
/// <param name="entry">The directory entry.</param>
/// <param name="compression">The embedded frame compression.</param>
/// <param name="bitsPerPixel">The embedded bitmap bit depth.</param>
/// <param name="colorTable">The embedded bitmap color table.</param>
protected abstract void SetFrameMetadata(
ImageMetadata imageMetadata,
ImageFrameMetadata frameMetadata,
@ -234,58 +270,45 @@ internal abstract class IconDecoderCore : ImageDecoderCore
BmpBitsPerPixel bitsPerPixel,
ReadOnlyMemory<Color>? colorTable);
/// <summary>
/// Reads the icon directory entries needed by the configured frame limit.
/// </summary>
/// <param name="stream">The source stream.</param>
[MemberNotNull(nameof(entries))]
protected void ReadHeader(Stream stream)
private void ReadHeader(Stream stream)
{
Span<byte> buffer = stackalloc byte[IconDirEntry.Size];
Span<byte> buffer = this.buffer;
// ICONDIR
_ = CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size);
CheckEndOfStream(stream.Read(buffer[..IconDir.Size]), IconDir.Size);
this.fileHeader = IconDir.Parse(buffer);
if (this.fileHeader.Reserved != 0 || this.fileHeader.Type != this.iconFileType || this.fileHeader.Count == 0)
{
throw new InvalidImageContentException("The icon directory header is invalid.");
}
// ICONDIRENTRY
this.entries = new IconDirEntry[this.fileHeader.Count];
int entryCount = (int)Math.Min(this.fileHeader.Count, this.Options.MaxFrames);
this.entries = new IconDirEntry[entryCount];
for (int i = 0; i < this.entries.Length; i++)
{
_ = CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size);
CheckEndOfStream(stream.Read(buffer[..IconDirEntry.Size]), IconDirEntry.Size);
this.entries[i] = IconDirEntry.Parse(buffer);
}
int width = 0;
int height = 0;
foreach (IconDirEntry entry in this.entries)
{
// Since Windows 95 size of an image in the ICONDIRENTRY structure might
// be set to zero, which means 256 pixels.
if (entry.Width == 0)
{
width = 256;
}
if (entry.Height == 0)
{
height = 256;
}
if (width == 256 && height == 256)
{
break;
}
width = Math.Max(width, entry.Width);
height = Math.Max(height, entry.Height);
}
this.Dimensions = new Size(width, height);
}
/// <summary>
/// Creates the decoder configured for an embedded PNG or headerless, double-height bitmap frame.
/// </summary>
/// <param name="isPng">Whether the embedded frame has a PNG signature.</param>
/// <returns>The configured frame decoder.</returns>
private ImageDecoderCore GetDecoder(bool isPng)
{
if (isPng)
{
return new PngDecoderCore(new PngDecoderOptions
{
GeneralOptions = this.Options,
GeneralOptions = this.Options
});
}
@ -294,17 +317,59 @@ internal abstract class IconDecoderCore : ImageDecoderCore
GeneralOptions = this.Options,
ProcessedAlphaMask = true,
SkipFileHeader = true,
UseDoubleHeight = true,
UseDoubleHeight = true
});
}
private static int CheckEndOfStream(int v, int length)
/// <summary>
/// Creates a seekable view bounded to one directory entry's declared payload.
/// </summary>
/// <param name="frameStream">The reusable bounded payload stream.</param>
/// <param name="stream">The containing icon stream.</param>
/// <param name="basePosition">The absolute start of the icon resource.</param>
/// <param name="entry">The directory entry describing the payload.</param>
private void SetFrameStreamBounds(IconFrameStream frameStream, BufferedReadStream stream, long basePosition, in IconDirEntry entry)
{
if (v != length)
long available = stream.Length - basePosition;
uint directorySize = (uint)(IconDir.Size + (this.fileHeader.Count * IconDirEntry.Size));
// Offsets are relative to the icon resource and must not point into its directory or beyond its containing stream.
if (entry.Reserved is not 0
|| entry.BytesInRes is 0
|| entry.ImageOffset < directorySize
|| entry.ImageOffset > available)
{
throw new InvalidImageContentException("Not enough bytes to read icon header.");
throw new InvalidImageContentException("The icon directory contains an invalid image resource range.");
}
return v;
long remaining = available - entry.ImageOffset;
long length = entry.BytesInRes;
if (length > remaining)
{
if (this.fileHeader.Count is not 1)
{
// Clamping a multi-entry resource could expose the next image payload to the current child decoder.
throw new InvalidImageContentException("The icon directory contains an invalid image resource range.");
}
// Some established single-image ICO files overstate BytesInRes but contain a complete payload.
// The containing stream is still a safe hard boundary because no sibling image can follow it.
length = remaining;
}
frameStream.Reset(basePosition + entry.ImageOffset, length);
}
/// <summary>
/// Ensures that a complete fixed-size directory structure was read.
/// </summary>
/// <param name="bytesRead">The number of bytes read.</param>
/// <param name="expectedLength">The required structure length.</param>
private static void CheckEndOfStream(int bytesRead, int expectedLength)
{
if (bytesRead != expectedLength)
{
throw new InvalidImageContentException("Not enough bytes to read icon header.");
}
}
}

38
src/ImageSharp/Formats/Icon/IconDir.cs

@ -1,14 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// Represents an ICO or CUR file directory header.
/// </summary>
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
internal struct IconDir
{
/// <summary>
/// The serialized directory-header size in bytes.
/// </summary>
public const int Size = 3 * sizeof(ushort);
/// <summary>
@ -26,26 +31,39 @@ internal struct IconDir
/// </summary>
public ushort Count;
/// <summary>
/// Initializes a new instance of the <see cref="IconDir"/> struct.
/// </summary>
/// <param name="type">The icon file type.</param>
public IconDir(IconFileType type)
: this(type, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="IconDir"/> struct.
/// </summary>
/// <param name="type">The icon file type.</param>
/// <param name="count">The number of directory entries.</param>
public IconDir(IconFileType type, ushort count)
: this(0, type, count)
{
}
public IconDir(ushort reserved, IconFileType type, ushort count)
{
this.Reserved = reserved;
this.Reserved = 0;
this.Type = type;
this.Count = count;
}
public static ref IconDir Parse(ReadOnlySpan<byte> data)
=> ref Unsafe.As<byte, IconDir>(ref MemoryMarshal.GetReference(data));
/// <summary>
/// Parses an icon directory header from its byte representation.
/// </summary>
/// <param name="data">The icon directory header data.</param>
/// <returns>The parsed icon directory header.</returns>
public static IconDir Parse(ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDir>(data)[0];
/// <summary>
/// Writes the icon directory header to the destination stream.
/// </summary>
/// <param name="stream">The destination stream.</param>
public readonly void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
=> stream.Write(MemoryMarshal.Cast<IconDir, byte>([this]));
}

25
src/ImageSharp/Formats/Icon/IconDirEntry.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Formats.Icon;
@ -9,6 +8,9 @@ namespace SixLabors.ImageSharp.Formats.Icon;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)]
internal struct IconDirEntry
{
/// <summary>
/// The serialized directory-entry size in bytes.
/// </summary>
public const int Size = (4 * sizeof(byte)) + (2 * sizeof(ushort)) + (2 * sizeof(uint));
/// <summary>
@ -17,7 +19,7 @@ internal struct IconDirEntry
public byte Width;
/// <summary>
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.[
/// Specifies image height in pixels. Can be any number between 0 and 255. Value 0 means image height is 256 pixels.
/// </summary>
public byte Height;
@ -44,7 +46,7 @@ internal struct IconDirEntry
public ushort BitCount;
/// <summary>
/// Specifies the size of the image's data in bytes
/// Specifies the size of the image's data in bytes.
/// </summary>
public uint BytesInRes;
@ -53,9 +55,18 @@ internal struct IconDirEntry
/// </summary>
public uint ImageOffset;
public static ref IconDirEntry Parse(in ReadOnlySpan<byte> data)
=> ref Unsafe.As<byte, IconDirEntry>(ref MemoryMarshal.GetReference(data));
/// <summary>
/// Parses an icon directory entry from its byte representation.
/// </summary>
/// <param name="data">The icon directory entry data.</param>
/// <returns>The parsed icon directory entry.</returns>
public static IconDirEntry Parse(ReadOnlySpan<byte> data)
=> MemoryMarshal.Cast<byte, IconDirEntry>(data)[0];
public readonly void WriteTo(in Stream stream)
=> stream.Write(MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in this, 1)));
/// <summary>
/// Writes the icon directory entry to the destination stream.
/// </summary>
/// <param name="stream">The destination stream.</param>
public readonly void WriteTo(Stream stream)
=> stream.Write(MemoryMarshal.Cast<IconDirEntry, byte>([this]));
}

248
src/ImageSharp/Formats/Icon/IconEncoderCore.cs

@ -1,111 +1,187 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using System.Buffers;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// Encodes ICO and CUR containers.
/// </summary>
internal abstract class IconEncoderCore
{
private readonly QuantizingImageEncoder encoder;
private readonly IconFileType iconFileType;
private IconDir fileHeader;
private EncodingFrameMetadata[]? entries;
/// <summary>
/// Initializes a new instance of the <see cref="IconEncoderCore"/> class.
/// </summary>
/// <param name="encoder">The encoder options.</param>
/// <param name="iconFileType">The icon container type.</param>
protected IconEncoderCore(QuantizingImageEncoder encoder, IconFileType iconFileType)
{
this.encoder = encoder;
this.iconFileType = iconFileType;
}
public void Encode<TPixel>(
Image<TPixel> image,
Stream stream,
CancellationToken cancellationToken)
/// <summary>
/// Supplies icon directory and color-table metadata without allocating intermediary metadata objects.
/// </summary>
internal interface IEncodingFrameMetadataProvider
{
/// <summary>
/// Gets the encoding metadata for a source frame.
/// </summary>
/// <param name="frame">The source frame.</param>
/// <param name="colorTable">The optional bitmap color table.</param>
/// <returns>The encoding metadata.</returns>
public EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable);
}
/// <summary>
/// Encodes all source frames using the metadata provider owned by the concrete icon format.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <typeparam name="TProvider">The metadata provider type.</typeparam>
/// <param name="image">The source image.</param>
/// <param name="stream">The destination stream.</param>
/// <param name="provider">The frame metadata provider.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
protected void Encode<TPixel, TProvider>(Image<TPixel> image, Stream stream, TProvider provider, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
where TProvider : struct, IEncodingFrameMetadataProvider
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
// Stream may not at 0.
// Directory metadata is unmanaged and short-lived, so allocator-owned storage avoids an array plus one object per frame.
using IMemoryOwner<EncodingFrameMetadata> owner = image.Configuration.MemoryAllocator.Allocate<EncodingFrameMetadata>(image.Frames.Count);
Span<EncodingFrameMetadata> entries = owner.GetSpan()[..image.Frames.Count];
this.Encode(image, stream, 0, entries, provider, cancellationToken);
}
/// <summary>
/// Encodes a contiguous source-frame range using a stack-only metadata provider.
/// </summary>
/// <typeparam name="TPixel">The source pixel type.</typeparam>
/// <typeparam name="TProvider">The metadata provider 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="entries">The directory metadata for the source frames.</param>
/// <param name="provider">The frame metadata provider.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
internal void Encode<TPixel, TProvider>(Image<TPixel> image, Stream stream, int frameIndex, Span<EncodingFrameMetadata> entries, TProvider provider, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
where TProvider : struct, IEncodingFrameMetadataProvider
{
if ((uint)entries.Length > ushort.MaxValue)
{
throw new ImageFormatException("ICO and CUR resources cannot contain more than 65535 directory entries.");
}
// Offsets stored in ICO/CUR entries are relative to the start of this child resource, not the containing ANI stream.
long basePosition = stream.Position;
this.InitHeader(image);
IconDir fileHeader = new(this.iconFileType, (ushort)entries.Length);
// We don't write the header and entries yet as we need to write the image data first.
int dataOffset = IconDir.Size + (IconDirEntry.Size * this.entries.Length);
// Reserve the directory first because BytesInRes and ImageOffset are known only after each payload is encoded.
int dataOffset = IconDir.Size + (IconDirEntry.Size * entries.Length);
_ = stream.Seek(dataOffset, SeekOrigin.Current);
for (int i = 0; i < image.Frames.Count; i++)
for (int i = 0; i < entries.Length; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// Since Windows Vista, the size of an image is determined from the BITMAPINFOHEADER structure or PNG image data
// which technically allows storing icons with larger than 256 pixels, but such larger sizes are not recommended by Microsoft.
ImageFrame<TPixel> frame = image.Frames[i];
int width = this.entries[i].Entry.Width;
ImageFrame<TPixel> frame = image.Frames[frameIndex + i];
// The struct provider is statically dispatched, avoiding boxing and intermediary ICO/CUR metadata allocations.
// Only unmanaged directory data survives until backpatching; the managed color table is consumed for this frame.
entries[i] = provider.GetEncodingFrameMetadata(frame, out ReadOnlyMemory<Color>? colorTable);
int width = entries[i].Entry.Width;
if (width is 0)
{
width = frame.Width;
}
int height = this.entries[i].Entry.Height;
int height = entries[i].Entry.Height;
if (height is 0)
{
height = frame.Height;
}
this.entries[i].Entry.ImageOffset = (uint)stream.Position;
long imageStart = stream.Position;
entries[i].Entry.ImageOffset = checked((uint)(imageStart - basePosition));
// We crop the frame to the size specified in the metadata.
using Image<TPixel> encodingFrame = new(width, height);
// ANI flattens variants onto a common canvas, while an icon entry can encode a smaller rectangle.
// Child encoders consume Image, so an isolated cropped image is required to prevent encoding the padded canvas.
using Image<TPixel> encodingFrame = new(image.Configuration, width, height);
for (int y = 0; y < height; y++)
{
frame.PixelBuffer.DangerousGetRowSpan(y)[..width]
.CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y));
frame.PixelBuffer.DangerousGetRowSpan(y)[..width].CopyTo(encodingFrame.GetRootFramePixelBuffer().DangerousGetRowSpan(y));
}
ref EncodingFrameMetadata encodingMetadata = ref this.entries[i];
ref EncodingFrameMetadata encodingMetadata = ref entries[i];
QuantizingImageEncoder encoder = encodingMetadata.Compression switch
// Compression and bitmap depth are per-entry, so the concrete encoder configuration must be selected per frame.
switch (encodingMetadata.Compression)
{
IconFrameCompression.Bmp => new BmpEncoder()
case IconFrameCompression.Bmp:
{
Quantizer = this.GetQuantizer(encodingMetadata),
ProcessedAlphaMask = true,
UseDoubleHeight = true,
SkipFileHeader = true,
SupportTransparency = false,
TransparentColorMode = this.encoder.TransparentColorMode,
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
BitsPerPixel = encodingMetadata.BmpBitsPerPixel
},
IconFrameCompression.Png => new PngEncoder()
BmpEncoder bmpEncoder = new()
{
Quantizer = this.GetQuantizer(encodingMetadata, colorTable),
ProcessedAlphaMask = true,
UseDoubleHeight = true,
SkipFileHeader = true,
SupportTransparency = false,
TransparentColorMode = this.encoder.TransparentColorMode,
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy,
BitsPerPixel = encodingMetadata.BmpBitsPerPixel,
SkipMetadata = this.encoder.SkipMetadata
};
BmpEncoderCore bmpEncoderCore = new(bmpEncoder, image.Configuration.MemoryAllocator);
bmpEncoderCore.Encode(encodingFrame, stream, cancellationToken);
break;
}
case IconFrameCompression.Png:
{
// Only 32bit Png supported.
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
BitDepth = PngBitDepth.Bit8,
ColorType = PngColorType.RgbWithAlpha,
TransparentColorMode = this.encoder.TransparentColorMode,
CompressionLevel = PngCompressionLevel.BestCompression
},
_ => throw new NotSupportedException(),
};
encoder.Encode(encodingFrame, stream);
encodingMetadata.Entry.BytesInRes = (uint)stream.Position - encodingMetadata.Entry.ImageOffset;
PngEncoder pngEncoder = new()
{
// Only 32bit Png supported.
// https://devblogs.microsoft.com/oldnewthing/20101022-00/?p=12473
BitDepth = PngBitDepth.Bit8,
ColorType = PngColorType.RgbWithAlpha,
TransparentColorMode = this.encoder.TransparentColorMode,
CompressionLevel = PngCompressionLevel.BestCompression,
SkipMetadata = this.encoder.SkipMetadata
};
using PngEncoderCore pngEncoderCore = new(image.Configuration, pngEncoder);
pngEncoderCore.Encode(encodingFrame, stream, cancellationToken);
break;
}
default:
throw new NotSupportedException();
}
encodingMetadata.Entry.BytesInRes = checked((uint)(stream.Position - imageStart));
}
// We now need to rewind the stream and write the header and the entries.
// Backpatch the reserved directory after every relative offset and payload length has been measured.
long endPosition = stream.Position;
_ = stream.Seek(basePosition, SeekOrigin.Begin);
this.fileHeader.WriteTo(stream);
foreach (EncodingFrameMetadata frame in this.entries)
fileHeader.WriteTo(stream);
foreach (EncodingFrameMetadata frame in entries)
{
frame.Entry.WriteTo(stream);
}
@ -113,31 +189,16 @@ internal abstract class IconEncoderCore
_ = stream.Seek(endPosition, SeekOrigin.Begin);
}
[MemberNotNull(nameof(entries))]
private void InitHeader(Image image)
{
this.fileHeader = new IconDir(this.iconFileType, (ushort)image.Frames.Count);
this.entries = this.iconFileType switch
{
IconFileType.ICO =>
[.. image.Frames.Select(i =>
{
IcoFrameMetadata metadata = i.Metadata.GetIcoMetadata();
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
})],
IconFileType.CUR =>
[.. image.Frames.Select(i =>
{
CurFrameMetadata metadata = i.Metadata.GetCurMetadata();
return new EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, metadata.ColorTable, metadata.ToIconDirEntry(i.Size));
})],
_ => throw new NotSupportedException(),
};
}
private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata)
/// <summary>
/// Gets the quantizer for an embedded bitmap frame.
/// </summary>
/// <param name="metadata">The frame encoding metadata.</param>
/// <param name="colorTable">The optional bitmap color table.</param>
/// <returns>The configured quantizer, or <see langword="null"/> when quantization is not required.</returns>
private IQuantizer? GetQuantizer(EncodingFrameMetadata metadata, ReadOnlyMemory<Color>? colorTable)
{
if (metadata.Entry.BitCount > 8)
// CUR stores its vertical hotspot in Entry.BitCount, so quantization must use the independent bitmap depth.
if (metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8)
{
return null;
}
@ -147,7 +208,7 @@ internal abstract class IconEncoderCore
return this.encoder.Quantizer;
}
if (metadata.ColorTable is null)
if (colorTable is null)
{
int count = metadata.Entry.ColorCount;
if (count == 0)
@ -162,33 +223,42 @@ internal abstract class IconEncoderCore
}
// Don't dither if we have a palette. We want to preserve as much information as possible.
return new PaletteQuantizer(metadata.ColorTable.Value, new QuantizerOptions { Dither = null });
return new PaletteQuantizer(colorTable.Value, new QuantizerOptions { Dither = null });
}
internal sealed class EncodingFrameMetadata
/// <summary>
/// Stores the unmanaged per-frame state required while an icon directory is backpatched.
/// </summary>
internal struct EncodingFrameMetadata
{
private IconDirEntry iconDirEntry;
/// <summary>
/// The icon directory entry.
/// </summary>
public IconDirEntry Entry;
public EncodingFrameMetadata(
IconFrameCompression compression,
BmpBitsPerPixel bmpBitsPerPixel,
ReadOnlyMemory<Color>? colorTable,
IconDirEntry iconDirEntry)
/// <summary>
/// Initializes a new instance of the <see cref="EncodingFrameMetadata"/> struct.
/// </summary>
/// <param name="compression">The embedded image compression.</param>
/// <param name="bmpBitsPerPixel">The bitmap bit depth.</param>
/// <param name="iconDirEntry">The icon directory entry.</param>
public EncodingFrameMetadata(IconFrameCompression compression, BmpBitsPerPixel bmpBitsPerPixel, IconDirEntry iconDirEntry)
{
this.Compression = compression;
this.BmpBitsPerPixel = compression == IconFrameCompression.Png
? BmpBitsPerPixel.Bit32
: bmpBitsPerPixel;
this.ColorTable = colorTable;
this.iconDirEntry = iconDirEntry;
this.Entry = iconDirEntry;
}
/// <summary>
/// Gets the embedded image compression.
/// </summary>
public IconFrameCompression Compression { get; }
/// <summary>
/// Gets the bitmap bit depth.
/// </summary>
public BmpBitsPerPixel BmpBitsPerPixel { get; }
public ReadOnlyMemory<Color>? ColorTable { get; set; }
public ref IconDirEntry Entry => ref this.iconDirEntry;
}
}

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

@ -4,17 +4,17 @@
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// Ico file type
/// Identifies the type stored in an ICO or CUR directory header.
/// </summary>
internal enum IconFileType : ushort
{
/// <summary>
/// ICO file
/// A Windows icon file.
/// </summary>
ICO = 1,
/// <summary>
/// CUR file
/// A Windows cursor file.
/// </summary>
CUR = 2
}

6
src/ImageSharp/Formats/Icon/IconFrameCompression.cs

@ -4,17 +4,17 @@
namespace SixLabors.ImageSharp.Formats.Icon;
/// <summary>
/// IconFrameCompression
/// Specifies the encoding used for an image embedded in an ICO or CUR resource.
/// </summary>
public enum IconFrameCompression
{
/// <summary>
/// Bmp
/// The image is encoded as a headerless Windows bitmap with an AND transparency mask.
/// </summary>
Bmp,
/// <summary>
/// Png
/// The image is encoded as PNG data.
/// </summary>
Png
}

114
src/ImageSharp/Formats/Icon/IconFrameStream.cs

@ -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();
}

66
src/ImageSharp/Formats/Icon/IconImageFormatDetector.cs

@ -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;
}
}

7
src/ImageSharp/Formats/Jpeg/Components/Decoder/ArithmeticScanDecoder.cs

@ -55,7 +55,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder
private ArithmeticDecodingTable[] acDecodingTables;
// Don't make this a ReadOnlySpan<byte>, as the values need to get updated.
private readonly byte[] fixedBin = [113, 0, 0, 0];
private InlineArray4<byte> fixedBin;
private readonly CancellationToken cancellationToken;
@ -194,6 +194,9 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder
this.spectralConverter = converter;
this.cancellationToken = cancellationToken;
// Inline storage is zero-initialized with the decoder; only the arithmetic probability state starts nonzero.
this.fixedBin[0] = 113;
this.c = 0;
this.a = 0;
this.ct = -16; // Force reading 2 initial bytes to fill C.
@ -233,7 +236,7 @@ internal class ArithmeticScanDecoder : IJpegScanDecoder
}
}
private ref byte GetFixedBinReference() => ref MemoryMarshal.GetArrayDataReference(this.fixedBin);
private ref byte GetFixedBinReference() => ref this.fixedBin[0];
/// <inheritdoc/>
public void ParseEntropyCodedData(int scanComponentCount, IccProfile iccProfile)

12
src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs

@ -10,7 +10,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
/// Represents a Huffman coding table containing basic coding data plus tables for accelerated computation.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct HuffmanTable
internal struct HuffmanTable
{
/// <summary>
/// Memory workspace buffer size used in <see cref="HuffmanTable"/> ctor.
@ -20,25 +20,25 @@ internal unsafe struct HuffmanTable
/// <summary>
/// Derived from the DHT marker. Contains the symbols, in order of incremental code length.
/// </summary>
public fixed byte Values[256];
public InlineArray256<byte> Values;
/// <summary>
/// Contains the largest code of length k (0 if none). MaxCode[17] is a sentinel to
/// ensure <see cref="JpegBitReader.DecodeHuffman"/> terminates.
/// </summary>
public fixed ulong MaxCode[18];
public InlineArray18<ulong> MaxCode;
/// <summary>
/// Values[] offset for codes of length k ValOffset[k] = Values[] index of 1st symbol of code length
/// k, less the smallest code of length k; so given a code of length k, the corresponding symbol is
/// Values[code + ValOffset[k]].
/// </summary>
public fixed int ValOffset[19];
public InlineArray19<int> ValOffset;
/// <summary>
/// Contains the length of bits for the given k value.
/// </summary>
public fixed byte LookaheadSize[JpegConstants.Huffman.LookupSize];
public InlineArray256<byte> LookaheadSize;
/// <summary>
/// Lookahead table: indexed by the next <see cref="JpegConstants.Huffman.LookupBits"/> bits of
@ -50,7 +50,7 @@ internal unsafe struct HuffmanTable
/// bits in the corresponding Huffman code, or <see cref="JpegConstants.Huffman.LookupBits"/> + 1
/// if too long. The next 8 bits of each entry contain the symbol.
/// </summary>
public fixed byte LookaheadValue[JpegConstants.Huffman.LookupSize];
public InlineArray256<byte> LookaheadValue;
/// <summary>
/// Initializes a new instance of the <see cref="HuffmanTable"/> struct.

42
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -43,7 +43,7 @@ internal sealed class PngEncoderCore : IDisposable
/// <summary>
/// Reusable buffer for writing chunk data.
/// </summary>
private ScratchBuffer chunkDataBuffer; // mutable struct, don't make readonly
private InlineArray26<byte> chunkDataBuffer; // mutable struct, don't make readonly
/// <summary>
/// The encoder with options
@ -734,9 +734,9 @@ internal sealed class PngEncoderCore : IDisposable
filterMethod: 0,
interlaceMethod: this.interlaceMode);
header.WriteTo(this.chunkDataBuffer.Span);
header.WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer.Span, 0, PngHeader.Size);
this.WriteChunk(stream, PngChunkType.Header, this.chunkDataBuffer, 0, PngHeader.Size);
}
/// <summary>
@ -749,9 +749,9 @@ internal sealed class PngEncoderCore : IDisposable
{
AnimationControl acTL = new(framesCount, playsCount);
acTL.WriteTo(this.chunkDataBuffer.Span);
acTL.WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer.Span, 0, AnimationControl.Size);
this.WriteChunk(stream, PngChunkType.AnimationControl, this.chunkDataBuffer, 0, AnimationControl.Size);
}
/// <summary>
@ -820,9 +820,9 @@ internal sealed class PngEncoderCore : IDisposable
return;
}
PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer.Span);
PngPhysical.FromMetadata(meta).WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer.Span, 0, PngPhysical.Size);
this.WriteChunk(stream, PngChunkType.Physical, this.chunkDataBuffer, 0, PngPhysical.Size);
}
/// <summary>
@ -1067,7 +1067,7 @@ internal sealed class PngEncoderCore : IDisposable
throw new NotSupportedException("CICP matrix coefficients other than Identity are not supported in PNG");
}
Span<byte> outputBytes = this.chunkDataBuffer.Span[..4];
Span<byte> outputBytes = this.chunkDataBuffer[..4];
outputBytes[0] = (byte)metaData.CicpProfile.ColorPrimaries;
outputBytes[1] = (byte)metaData.CicpProfile.TransferCharacteristics;
outputBytes[2] = (byte)metaData.CicpProfile.MatrixCoefficients;
@ -1222,9 +1222,9 @@ internal sealed class PngEncoderCore : IDisposable
// 4-byte unsigned integer of gamma * 100,000.
uint gammaValue = (uint)(this.gamma * 100_000F);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue);
BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer[..4], gammaValue);
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4);
this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer, 0, 4);
}
}
@ -1241,7 +1241,7 @@ internal sealed class PngEncoderCore : IDisposable
return;
}
Span<byte> alpha = this.chunkDataBuffer.Span;
Span<byte> alpha = this.chunkDataBuffer;
if (pngMetadata.ColorType == PngColorType.Rgb)
{
if (this.use16Bit)
@ -1251,7 +1251,7 @@ internal sealed class PngEncoderCore : IDisposable
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G);
BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6);
}
else
{
@ -1260,7 +1260,7 @@ internal sealed class PngEncoderCore : IDisposable
alpha[1] = rgb.R;
alpha[3] = rgb.G;
alpha[5] = rgb.B;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 6);
}
}
else if (pngMetadata.ColorType == PngColorType.Grayscale)
@ -1269,14 +1269,14 @@ internal sealed class PngEncoderCore : IDisposable
{
L16 l16 = pngMetadata.TransparentColor.Value.ToPixel<L16>();
BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2);
}
else
{
L8 l8 = pngMetadata.TransparentColor.Value.ToPixel<L8>();
alpha.Clear();
alpha[1] = l8.PackedValue;
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2);
this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer, 0, 2);
}
}
}
@ -1301,9 +1301,9 @@ internal sealed class PngEncoderCore : IDisposable
disposalMode: frameMetadata.DisposalMode,
blendMode: frameMetadata.BlendMode);
fcTL.WriteTo(this.chunkDataBuffer.Span);
fcTL.WriteTo(this.chunkDataBuffer);
this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer.Span, 0, FrameControl.Size);
this.WriteChunk(stream, PngChunkType.FrameControl, this.chunkDataBuffer, 0, FrameControl.Size);
return fcTL;
}
@ -1834,12 +1834,4 @@ internal sealed class PngEncoderCore : IDisposable
// PngColorType.RgbWithAlpha
_ => use16Bit ? 8 : 4,
};
private unsafe struct ScratchBuffer
{
private const int Size = 26;
private fixed byte scratch[Size];
public Span<byte> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
}

6
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -100,7 +100,7 @@ internal abstract class BitWriterBase
bool hasAnimation)
{
// Write file size later
RiffHelper.BeginWriteRiff(stream, WebpConstants.WebpFormTypeFourCc);
RiffHelper.BeginWriteRiffFile(stream, WebpConstants.WebpFourCc);
// Write VP8X, header if necessary.
WebpVp8X vp8x = default;
@ -151,7 +151,7 @@ internal abstract class BitWriterBase
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Xmp, xmpProfile.Data);
}
RiffHelper.EndWriteVp8X(stream, in vp8x, updateVp8x, initialPosition);
RiffHelper.EndWriteRiffFile(stream, in vp8x, updateVp8x, initialPosition);
}
/// <summary>
@ -189,7 +189,7 @@ internal abstract class BitWriterBase
stream.WriteByte(flags);
stream.Write(dataBytes);
RiffHelper.EndWriteChunk(stream, pos, 2);
RiffHelper.EndWriteChunk(stream, pos);
}
/// <summary>

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

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

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

@ -24,7 +24,7 @@ internal class Vp8LEncoder : IDisposable
/// <summary>
/// Scratch buffer to reduce allocations.
/// </summary>
private ScratchBuffer scratch; // mutable struct, don't make readonly
private InlineArray256<int> scratch; // mutable struct, don't make readonly
private readonly int[][] histoArgb = [new int[256], new int[256], new int[256], new int[256]];
@ -315,7 +315,7 @@ internal class Vp8LEncoder : IDisposable
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition, 2);
RiffHelper.EndWriteChunk(stream, prevPosition);
}
return hasAlpha;
@ -803,7 +803,7 @@ internal class Vp8LEncoder : IDisposable
int transformWidth = LosslessUtils.SubSampleSize(width, colorTransformBits);
int transformHeight = LosslessUtils.SubSampleSize(height, colorTransformBits);
PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch.Span);
PredictorEncoder.ColorSpaceTransform(width, height, colorTransformBits, this.quality, this.EncodedData.GetSpan(), this.TransformData.GetSpan(), this.scratch);
this.bitWriter.PutBits(WebpConstants.TransformPresent, 1);
this.bitWriter.PutBits((uint)Vp8LTransformType.CrossColorTransform, 2);
@ -876,7 +876,7 @@ internal class Vp8LEncoder : IDisposable
private void StoreHuffmanCode(Span<HuffmanTree> huffTree, HuffmanTreeToken[] tokens, HuffmanTreeCode huffmanCode)
{
int count = 0;
Span<int> symbols = this.scratch.Span[..2];
Span<int> symbols = this.scratch[..2];
symbols.Clear();
const int maxBits = 8;
const int maxSymbol = 1 << maxBits;
@ -1915,15 +1915,4 @@ internal class Vp8LEncoder : IDisposable
this.HashChain.Dispose();
}
/// <summary>
/// Scratch buffer to reduce allocations.
/// </summary>
private unsafe struct ScratchBuffer
{
private const int Size = 256;
private fixed int scratch[Size];
public Span<int> Span => MemoryMarshal.CreateSpan(ref this.scratch[0], Size);
}
}

10
src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs

@ -35,7 +35,7 @@ internal static unsafe class QuantEnc
int lambda = dqm.LambdaI16;
int tlambda = dqm.TLambda;
Span<byte> src = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc);
Span<int> scratch = it.Scratch3;
Span<int> scratch = it.Scratch3.AsSpan();
Vp8ModeScore rdTmp = new();
Vp8Residual res = new();
Vp8ModeScore rdCur = rdTmp;
@ -105,7 +105,7 @@ internal static unsafe class QuantEnc
int tlambda = dqm.TLambda;
Span<byte> src0 = it.YuvIn.AsSpan(Vp8EncIterator.YOffEnc);
Span<byte> bestBlocks = it.YuvOut2.AsSpan(Vp8EncIterator.YOffEnc);
Span<int> scratch = it.Scratch3;
Span<int> scratch = it.Scratch3.AsSpan();
int totalHeaderBits = 0;
Vp8ModeScore rdBest = new();
@ -280,7 +280,7 @@ internal static unsafe class QuantEnc
int nz = 0;
int n;
Span<short> shortScratchSpan = it.Scratch2.AsSpan();
Span<int> scratch = it.Scratch3.AsSpan(0, 16);
Span<int> scratch = it.Scratch3.AsSpan();
shortScratchSpan.Clear();
scratch.Clear();
Span<short> dcTmp = shortScratchSpan[..16];
@ -321,7 +321,7 @@ internal static unsafe class QuantEnc
{
Span<byte> reference = it.YuvP.AsSpan(Vp8Encoding.Vp8I4ModeOffsets[mode]);
Span<short> tmp = it.Scratch2.AsSpan(0, 16);
Span<int> scratch = it.Scratch3.AsSpan(0, 16);
Span<int> scratch = it.Scratch3.AsSpan();
Vp8Encoding.FTransform(src, reference, tmp, scratch);
int nz = QuantizeBlock(tmp, levels, ref dqm.Y1);
Vp8Encoding.ITransformOne(reference, tmp, yuvOut, scratch);
@ -336,7 +336,7 @@ internal static unsafe class QuantEnc
int nz = 0;
int n;
Span<short> tmp = it.Scratch2.AsSpan(0, 8 * 16);
Span<int> scratch = it.Scratch3.AsSpan(0, 16);
Span<int> scratch = it.Scratch3.AsSpan();
for (n = 0; n < 8; n += 2)
{

12
src/ImageSharp/Formats/Webp/Lossy/Vp8Decoder.cs

@ -13,8 +13,13 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
/// </summary>
internal class Vp8Decoder : IDisposable
{
private const int UpsamplingBufferSize = (14 * 32) + 15;
private Vp8MacroBlock leftMacroBlock;
// Vector upsampling needs 15 bytes of left padding, 128 interleaved UV bytes, two 128-byte BGR rows, and two 32-byte luma rows.
private readonly IMemoryOwner<byte> upsamplingBuffer;
/// <summary>
/// Initializes a new instance of the <see cref="Vp8Decoder"/> class.
/// </summary>
@ -74,6 +79,7 @@ internal class Vp8Decoder : IDisposable
this.TmpYBuffer = memoryAllocator.Allocate<byte>((int)width);
this.TmpUBuffer = memoryAllocator.Allocate<byte>((int)width);
this.TmpVBuffer = memoryAllocator.Allocate<byte>((int)width);
this.upsamplingBuffer = memoryAllocator.Allocate<byte>(UpsamplingBufferSize);
this.Pixels = memoryAllocator.Allocate<byte>((int)(width * height * 4), AllocationOptions.Clean);
#if DEBUG
@ -238,6 +244,11 @@ internal class Vp8Decoder : IDisposable
/// </summary>
public IMemoryOwner<byte> Pixels { get; }
/// <summary>
/// Gets the reusable workspace used while upsampling YUV rows.
/// </summary>
public Span<byte> UpsamplingBuffer => this.upsamplingBuffer.Memory.Span[..UpsamplingBufferSize];
/// <summary>
/// Gets or sets filter info.
/// </summary>
@ -339,6 +350,7 @@ internal class Vp8Decoder : IDisposable
this.TmpYBuffer.Dispose();
this.TmpUBuffer.Dispose();
this.TmpVBuffer.Dispose();
this.upsamplingBuffer.Dispose();
this.Pixels.Dispose();
}
}

2
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -517,7 +517,7 @@ internal class Vp8Encoder : IDisposable
if (hasAnimation)
{
RiffHelper.EndWriteChunk(stream, prevPosition, 2);
RiffHelper.EndWriteChunk(stream, prevPosition);
}
}
finally

12
src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs

@ -3,7 +3,7 @@
namespace SixLabors.ImageSharp.Formats.Webp.Lossy;
internal unsafe struct Vp8Matrix
internal struct Vp8Matrix
{
// [luma-ac,luma-dc,chroma][dc,ac]
private static readonly int[][] BiasMatrices =
@ -21,27 +21,27 @@ internal unsafe struct Vp8Matrix
/// <summary>
/// The quantizer steps.
/// </summary>
public fixed ushort Q[16];
public InlineArray16<ushort> Q;
/// <summary>
/// The reciprocals, fixed point.
/// </summary>
public fixed ushort IQ[16];
public InlineArray16<ushort> IQ;
/// <summary>
/// The rounding bias.
/// </summary>
public fixed uint Bias[16];
public InlineArray16<uint> Bias;
/// <summary>
/// The value below which a coefficient is zeroed.
/// </summary>
public fixed uint ZThresh[16];
public InlineArray16<uint> ZThresh;
/// <summary>
/// The frequency boosters for slight sharpening.
/// </summary>
public fixed short Sharpen[16];
public InlineArray16<short> Sharpen;
// Sharpening by (slightly) raising the hi-frequency coeffs.
// Hack-ish but helpful for mid-bitrate range. Use with care.

2
src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs

@ -733,7 +733,7 @@ internal sealed class WebpLossyDecoder
int mbw = io.MbW;
int uvw = (mbw + 1) >> 1; // >> 1 is bit-hack for / 2
int y = io.MbY;
byte[] uvBuffer = new byte[(14 * 32) + 15];
Span<byte> uvBuffer = dec.UpsamplingBuffer;
if (y == 0)
{

8
src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs

@ -27,7 +27,7 @@ internal static class YuvConversion
// we interpolate u/v as:
// ([9*a + 3*b + 3*c + d 3*a + 9*b + 3*c + d] + [8 8]) / 16
// ([3*a + b + 9*c + 3*d a + 3*b + 3*c + 9*d] [8 8]) / 16
public static void UpSample(Span<byte> topY, Span<byte> bottomY, Span<byte> topU, Span<byte> topV, Span<byte> curU, Span<byte> curV, Span<byte> topDst, Span<byte> bottomDst, int len, byte[] uvBuffer)
public static void UpSample(Span<byte> topY, Span<byte> bottomY, Span<byte> topU, Span<byte> topV, Span<byte> curU, Span<byte> curV, Span<byte> topDst, Span<byte> bottomDst, int len, Span<byte> uvBuffer)
{
if (Vector128.IsHardwareAccelerated)
{
@ -107,11 +107,11 @@ internal static class YuvConversion
//
// Then m can be written as
// m = (k + t + 1) / 2 - (((b^c) & (s^t)) | (k^t)) & 1
private static void UpSampleVector128(Span<byte> topY, Span<byte> bottomY, Span<byte> topU, Span<byte> topV, Span<byte> curU, Span<byte> curV, Span<byte> topDst, Span<byte> bottomDst, int len, byte[] uvBuffer)
private static void UpSampleVector128(Span<byte> topY, Span<byte> bottomY, Span<byte> topU, Span<byte> topV, Span<byte> curU, Span<byte> curV, Span<byte> topDst, Span<byte> bottomDst, int len, Span<byte> uvBuffer)
{
const int xStep = 3;
Array.Clear(uvBuffer);
Span<byte> ru = uvBuffer.AsSpan(15);
uvBuffer.Clear();
Span<byte> ru = uvBuffer[15..];
Span<byte> rv = ru[32..];
// Treat the first pixel in regular way.

16
src/ImageSharp/Formats/Webp/RiffChunkHeader.cs

@ -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));
}

126
src/ImageSharp/Formats/Webp/RiffHelper.cs

@ -2,93 +2,137 @@
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
namespace SixLabors.ImageSharp.Formats.Webp;
internal static class RiffHelper
{
public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan<byte> data)
{
long pos = BeginWriteChunk(stream, fourCc);
stream.Write(data);
EndWriteChunk(stream, pos);
}
/// <summary>
/// The header bytes identifying RIFF file.
/// </summary>
private const uint RiffFourCc = 0x52_49_46_46;
public static void WriteChunk<TStruct>(Stream stream, uint fourCc, in TStruct chunk)
where TStruct : unmanaged =>
WriteChunk(stream, fourCc, MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(in chunk, 1)));
public static void WriteRiffFile(Stream stream, string formType, Action<Stream> func) =>
WriteChunk(stream, RiffFourCc, s =>
{
s.Write(Encoding.ASCII.GetBytes(formType));
func(s);
});
public static long BeginWriteChunk(Stream stream, ReadOnlySpan<byte> fourCc)
public static void WriteChunk(Stream stream, uint fourCc, Action<Stream> func)
{
Span<byte> buffer = stackalloc byte[4];
// write the fourCC
stream.Write(fourCc);
BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
stream.Write(buffer);
long sizePosition = stream.Position;
// Leaving the place for the size
stream.Position += 4;
return sizePosition;
func(stream);
long position = stream.Position;
uint dataSize = (uint)(position - sizePosition - 4);
// padding
if (dataSize % 2 == 1)
{
stream.WriteByte(0);
position++;
}
BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
stream.Position = sizePosition;
stream.Write(buffer);
stream.Position = position;
}
public static long BeginWriteChunk(Stream stream, uint fourCc)
public static void WriteChunk(Stream stream, uint fourCc, ReadOnlySpan<byte> data)
{
Span<byte> buffer = stackalloc byte[4];
// write the fourCC
BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
return BeginWriteChunk(stream, buffer);
stream.Write(buffer);
uint size = (uint)data.Length;
BinaryPrimitives.WriteUInt32LittleEndian(buffer, size);
stream.Write(buffer);
stream.Write(data);
// padding
if (size % 2 is 1)
{
stream.WriteByte(0);
}
}
public static long BeginWriteRiff(Stream stream, ReadOnlySpan<byte> formType)
public static unsafe void WriteChunk<TStruct>(Stream stream, uint fourCc, in TStruct chunk)
where TStruct : unmanaged
{
long sizePosition = BeginWriteChunk(stream, "RIFF"u8);
stream.Write(formType);
return sizePosition;
fixed (TStruct* ptr = &chunk)
{
WriteChunk(stream, fourCc, new Span<byte>(ptr, sizeof(TStruct)));
}
}
public static long BeginWriteList(Stream stream, ReadOnlySpan<byte> listType)
public static long BeginWriteChunk(Stream stream, uint fourCc)
{
long sizePosition = BeginWriteChunk(stream, "LIST"u8);
stream.Write(listType);
Span<byte> buffer = stackalloc byte[4];
// write the fourCC
BinaryPrimitives.WriteUInt32BigEndian(buffer, fourCc);
stream.Write(buffer);
long sizePosition = stream.Position;
stream.Position += 4;
return sizePosition;
}
public static void EndWriteChunk(Stream stream, long sizePosition, int alignment = 1)
public static void EndWriteChunk(Stream stream, long sizePosition)
{
Guard.MustBeGreaterThan(alignment, 0, nameof(alignment));
Span<byte> buffer = stackalloc byte[4];
long currentPosition = stream.Position;
long position = stream.Position;
uint dataSize = (uint)(currentPosition - sizePosition - 4);
uint dataSize = (uint)(position - sizePosition - 4);
// Add padding
while (dataSize % alignment is not 0)
// padding
if (dataSize % 2 is 1)
{
stream.WriteByte(0);
dataSize++;
currentPosition++;
position++;
}
// Add the size of the encoded file to the Riff header.
BinaryPrimitives.WriteUInt32LittleEndian(buffer, dataSize);
stream.Position = sizePosition;
stream.Write(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<uint, byte>(ref dataSize), sizeof(uint)));
stream.Position = currentPosition;
stream.Write(buffer);
stream.Position = position;
}
public static long BeginWriteRiffFile(Stream stream, string formType)
{
long sizePosition = BeginWriteChunk(stream, RiffFourCc);
stream.Write(Encoding.ASCII.GetBytes(formType));
return sizePosition;
}
public static void EndWriteVp8X(Stream stream, in WebpVp8X vp8X, bool updateVp8X, long initPosition)
public static void EndWriteRiffFile(Stream stream, in WebpVp8X vp8x, bool updateVp8x, long sizePosition)
{
// Jump through "RIFF" fourCC
EndWriteChunk(stream, initPosition + 4, 2);
EndWriteChunk(stream, sizePosition + 4);
// Write the VP8X chunk if necessary.
if (updateVp8X)
if (updateVp8x)
{
long position = stream.Position;
stream.Position = initPosition + 12;
vp8X.WriteTo(stream);
stream.Position = sizePosition + 12;
vp8x.WriteTo(stream);
stream.Position = position;
}
}

26
src/ImageSharp/Formats/Webp/RiffOrListChunkHeader.cs

@ -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));
}

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

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

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

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

103
src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
// <auto-generated />
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Gif;
@ -22,6 +23,108 @@ namespace SixLabors.ImageSharp;
/// </summary>
public static partial class ImageExtensions
{
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsAni(this Image source, string path) => SaveAsAni(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsAniAsync(this Image source, string path) => SaveAsAniAsync(source, path, default);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsAniAsync(this Image source, string path, CancellationToken cancellationToken)
=> SaveAsAniAsync(source, path, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
public static void SaveAsAni(this Image source, string path, AniEncoder encoder) =>
source.Save(
path,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="path">The file path to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the path is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsAniAsync(this Image source, string path, AniEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
path,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsAni(this Image source, Stream stream)
=> SaveAsAni(source, stream, default);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsAniAsync(this Image source, Stream stream, CancellationToken cancellationToken = default)
=> SaveAsAniAsync(source, stream, default, cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
public static void SaveAsAni(this Image source, Stream stream, AniEncoder encoder)
=> source.Save(
stream,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance));
/// <summary>
/// Saves the image to the given stream with the Ani format.
/// </summary>
/// <param name="source">The image this method extends.</param>
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task SaveAsAniAsync(this Image source, Stream stream, AniEncoder encoder, CancellationToken cancellationToken = default)
=> source.SaveAsync(
stream,
encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(AniFormat.Instance),
cancellationToken);
/// <summary>
/// Saves the image to the given stream with the Bmp format.
/// </summary>

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

@ -3,6 +3,7 @@
// <auto-generated />
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Gif;
@ -14,7 +15,6 @@ using SixLabors.ImageSharp.Formats.Qoi;
using SixLabors.ImageSharp.Formats.Tga;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Exr;
namespace SixLabors.ImageSharp;
@ -24,6 +24,26 @@ namespace SixLabors.ImageSharp;
/// </summary>
public static class ImageMetadataExtensions
{
/// <summary>
/// Gets the <see cref="AniMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
/// or the requested format default constructor.
/// This instance will be added to the metadata for future requests.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="AniMetadata"/>
/// </returns>
public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="AniMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetAniMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="AniMetadata"/></returns>
public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
/// <summary>
/// Gets the <see cref="BmpMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
@ -104,26 +124,6 @@ public static class ImageMetadataExtensions
/// <returns>The new <see cref="IcoMetadata"/></returns>
public static IcoMetadata CloneIcoMetadata(this ImageMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the <see cref="AniMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
/// or the requested format default constructor.
/// This instance will be added to the metadata for future requests.
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>
/// The <see cref="AniMetadata"/>
/// </returns>
public static AniMetadata GetAniMetadata(this ImageMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetIcoMetadata(ImageMetadata)"/>
/// </summary>
/// <param name="source">The image metadata.</param>
/// <returns>The new <see cref="IcoMetadata"/></returns>
public static AniMetadata CloneAniMetadata(this ImageMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
/// <summary>
/// Gets the <see cref="JpegMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
@ -286,47 +286,47 @@ public static class ImageMetadataExtensions
/// <summary>
/// Gets the <see cref="CurFrameMetadata"/> from <paramref name="source"/>.<br/>
/// Gets the <see cref="AniFrameMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
/// or the requested format default constructor.
/// This instance will be added to the metadata for future requests.
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>
/// The <see cref="CurFrameMetadata"/>
/// The <see cref="AniFrameMetadata"/>
/// </returns>
public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance);
public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="CurMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetCurMetadata(ImageFrameMetadata)"/>
/// Creates a new cloned instance of <see cref="AniMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetAniMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="CurFrameMetadata"/></returns>
public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance);
/// <returns>The new <see cref="AniFrameMetadata"/></returns>
public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
/// <summary>
/// Gets the <see cref="IcoFrameMetadata"/> from <paramref name="source"/>.<br/>
/// Gets the <see cref="CurFrameMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
/// or the requested format default constructor.
/// This instance will be added to the metadata for future requests.
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>
/// The <see cref="IcoFrameMetadata"/>
/// The <see cref="CurFrameMetadata"/>
/// </returns>
public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance);
public static CurFrameMetadata GetCurMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(CurFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetIcoMetadata(ImageFrameMetadata)"/>
/// Creates a new cloned instance of <see cref="CurMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetCurMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="IcoFrameMetadata"/></returns>
public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <returns>The new <see cref="CurFrameMetadata"/></returns>
public static CurFrameMetadata CloneCurMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(CurFormat.Instance);
/// <summary>
/// Gets the <see cref="AniFrameMetadata"/> from <paramref name="source"/>.<br/>
/// Gets the <see cref="IcoFrameMetadata"/> from <paramref name="source"/>.<br/>
/// If none is found, an instance is created either by conversion from the decoded image format metadata
/// or the requested format default constructor.
/// This instance will be added to the metadata for future requests.
@ -335,15 +335,15 @@ public static class ImageMetadataExtensions
/// <returns>
/// The <see cref="IcoFrameMetadata"/>
/// </returns>
public static AniFrameMetadata GetAniMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(AniFormat.Instance);
public static IcoFrameMetadata GetIcoMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Creates a new cloned instance of <see cref="IcoMetadata"/> from the <paramref name="source"/>.
/// The instance is created via <see cref="GetAniMetadata(ImageFrameMetadata)"/>
/// The instance is created via <see cref="GetIcoMetadata(ImageFrameMetadata)"/>
/// </summary>
/// <param name="source">The image frame metadata.</param>
/// <returns>The new <see cref="IcoFrameMetadata"/></returns>
public static AniFrameMetadata CloneAniMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(AniFormat.Instance);
public static IcoFrameMetadata CloneIcoMetadata(this ImageFrameMetadata source) => source.CloneFormatMetadata(IcoFormat.Instance);
/// <summary>
/// Gets the <see cref="GifFrameMetadata"/> from <paramref name="source"/>.<br/>

2
src/ImageSharp/Formats/_Generated/_Formats.ttinclude

@ -4,6 +4,7 @@
// Licensed under the Six Labors Split License.
<#+
private static readonly string[] formats = [
"Ani",
"Bmp",
"Cur",
"Gif",
@ -19,6 +20,7 @@
];
private static readonly string[] frameFormats = [
"Ani",
"Cur",
"Ico",
"Gif",

52
src/ImageSharp/IO/BufferedReadStream.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.IO;
@ -23,14 +22,10 @@ internal sealed class BufferedReadStream : Stream
private readonly unsafe byte* pinnedReadBuffer;
/// <summary>
/// Index within our buffer, not reader position.
/// </summary>
// Index within our buffer, not reader position.
private int readBufferIndex;
/// <summary>
/// Matches what the stream position would be without buffering
/// </summary>
// Matches what the stream position would be without buffering
private long readerPosition;
private bool isDisposed;
@ -199,49 +194,6 @@ internal sealed class BufferedReadStream : Stream
return this.ReadToBufferViaCopyFast(buffer);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool TryReadUnmanaged<T>(out T result)
where T : unmanaged
{
this.cancellationToken.ThrowIfCancellationRequested();
int size = Unsafe.SizeOf<T>();
if (size > this.BufferSize)
{
Span<byte> span = stackalloc byte[size];
if (this.ReadToBufferDirectSlow(span) != size)
{
result = default;
return false;
}
result = MemoryMarshal.Read<T>(span);
}
else
{
if ((uint)this.readBufferIndex > (uint)(this.BufferSize - size))
{
this.FillReadBuffer();
}
if (this.GetCopyCount(size) != size)
{
this.EofHitCount++;
result = default;
return false;
}
Span<byte> span = this.readBuffer.AsSpan(this.readBufferIndex, size);
this.readerPosition += size;
this.readBufferIndex += size;
result = MemoryMarshal.Read<T>(span);
}
return true;
}
/// <inheritdoc/>
public override void Flush()
{

6
tests/Directory.Build.targets

@ -19,7 +19,11 @@
<ItemGroup>
<!-- Test Dependencies -->
<PackageReference Update="Colourful" Version="3.2.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<!--
Magick.NET 14.15.0 disables WebP-compressed TIFF support on Windows.
https://github.com/ImageMagick/tiff/commit/978181b6c999ee013f1b6df0a010d8c550d378cf
-->
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="14.14.0" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="10.0.0-beta.25563.105" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="8.0.0-beta.23580.1" />
<PackageReference Update="Moq" Version="4.20.72" />

2
tests/ImageSharp.Tests/ConfigurationTests.cs

@ -20,7 +20,7 @@ public class ConfigurationTests
public Configuration DefaultConfiguration { get; }
private readonly int expectedDefaultConfigurationCount = 12;
private readonly int expectedDefaultConfigurationCount = 13;
public ConfigurationTests()
{

134
tests/ImageSharp.Tests/Formats/Ani/AniDecoderTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.PixelFormats;
using static SixLabors.ImageSharp.Tests.TestImages.Ani;
@ -11,12 +13,136 @@ namespace SixLabors.ImageSharp.Tests.Formats.Ani;
[ValidateDisposedMemoryAllocations]
public class AniDecoderTests
{
/// <summary>
/// Verifies that ANI animation steps and embedded CUR resolution variants are flattened with their ANI metadata.
/// </summary>
[Theory]
[WithFile(Work, PixelTypes.Rgba32)]
[WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32)]
[WithFile(Help, PixelTypes.Rgba32)]
public void AniDecoder_Decode(TestImageProvider<Rgba32> provider)
[WithFile(Work, PixelTypes.Rgba32, 17, 1, 6U, 6U)]
[WithFile(MultiFramesInEveryIconChunk, PixelTypes.Rgba32, 54, 3, 3U, 3U)]
[WithFile(Help, PixelTypes.Rgba32, 4, 1, 10U, 12U)]
public void AniDecoder_Decode(
TestImageProvider<Rgba32> provider,
int expectedFrameCount,
int variantsPerStep,
uint expectedDisplayRate,
uint expectedFrameDelay)
{
using Image<Rgba32> image = provider.GetImage(AniDecoder.Instance);
Assert.Equal(expectedFrameCount, image.Frames.Count);
Assert.Equal(expectedDisplayRate, image.Metadata.GetAniMetadata().DisplayRate);
for (int i = 0; i < image.Frames.Count; i++)
{
AniFrameMetadata metadata = image.Frames[i].Metadata.GetAniMetadata();
Assert.Equal((i / variantsPerStep) + 1, metadata.SequenceNumber);
Assert.Equal(expectedFrameDelay, metadata.FrameDelay);
Assert.Equal(AniFrameFormat.Cur, metadata.FrameFormat);
Assert.NotEqual(0, (int)metadata.BmpBitsPerPixel);
}
}
/// <summary>
/// Verifies that identification exposes the same flattened ANI frame structure without decoding pixels.
/// </summary>
[Theory]
[InlineData(Work, 17)]
[InlineData(MultiFramesInEveryIconChunk, 54)]
[InlineData(Help, 4)]
public void AniDecoder_Identify(string path, int expectedFrameCount)
{
TestFile file = TestFile.Create(path);
using MemoryStream stream = new(file.Bytes, false);
ImageInfo info = AniDecoder.Instance.Identify(DecoderOptions.Default, stream);
Assert.Equal(expectedFrameCount, info.FrameMetadataCollection.Count);
for (int i = 0; i < info.FrameMetadataCollection.Count; i++)
{
Assert.True(info.FrameMetadataCollection[i].GetAniMetadata().SequenceNumber > 0);
}
}
/// <summary>
/// Verifies that invalid sequence metadata follows the ancillary-segment integrity policy.
/// </summary>
[Fact]
public void AniDecoder_InvalidSequenceReference_FollowsIntegrityHandling()
{
byte[] data = TestFile.Create(Help).Bytes.ToArray();
int sequenceOffset = data.AsSpan().IndexOf("seq "u8);
Assert.True(sequenceOffset >= 0);
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sequenceOffset + AniConstants.ChunkHeaderSize), uint.MaxValue);
using MemoryStream strictStream = new(data, false);
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
using MemoryStream ignoreStream = new(data, false);
DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreAncillary };
using Image<Rgba32> image = AniDecoder.Instance.Decode<Rgba32>(ignore, ignoreStream);
Assert.Equal(3, image.Frames.Count);
}
/// <summary>
/// Verifies that ignored corrupt resources retain their sequence-table slot.
/// </summary>
[Fact]
public void AniDecoder_UnsupportedResource_FollowsIntegrityHandling()
{
byte[] data = TestFile.Create(Work).Bytes.ToArray();
int resourceOffset = data.AsSpan().IndexOf("icon"u8);
Assert.True(resourceOffset >= 0);
int iconTypeOffset = resourceOffset + AniConstants.ChunkHeaderSize + sizeof(ushort);
BinaryPrimitives.WriteUInt16LittleEndian(data.AsSpan(iconTypeOffset), ushort.MaxValue);
using MemoryStream strictStream = new(data, false);
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
using MemoryStream ignoreStream = new(data, false);
DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
using Image<Rgba32> image = AniDecoder.Instance.Decode<Rgba32>(ignore, ignoreStream);
Assert.Equal(16, image.Frames.Count);
Assert.Equal(2, image.Frames.RootFrame.Metadata.GetAniMetadata().SequenceNumber);
}
/// <summary>
/// Verifies that a malformed rate chunk follows the ancillary-segment integrity policy.
/// </summary>
[Fact]
public void AniDecoder_InvalidRateChunk_FollowsIntegrityHandling()
{
byte[] source = TestFile.Create(Help).Bytes.ToArray();
int rateOffset = source.AsSpan().IndexOf("rate"u8);
Assert.True(rateOffset >= 0);
int rateSizeOffset = rateOffset + sizeof(uint);
int rateSize = (int)BinaryPrimitives.ReadUInt32LittleEndian(source.AsSpan(rateSizeOffset));
int rateEnd = rateOffset + AniConstants.ChunkHeaderSize + rateSize;
byte[] data = new byte[source.Length + 2];
source.AsSpan(0, rateEnd).CopyTo(data);
source.AsSpan(rateEnd).CopyTo(data.AsSpan(rateEnd + 2));
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(rateSizeOffset), (uint)rateSize + 2);
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(sizeof(uint)), (uint)data.Length - 8);
using MemoryStream defaultStream = new(data, false);
using Image<Rgba32> image = AniDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, defaultStream);
Assert.Equal(4, image.Frames.Count);
foreach (ImageFrame<Rgba32> frame in image.Frames)
{
Assert.Equal(10U, frame.Metadata.GetAniMetadata().FrameDelay);
}
using MemoryStream strictStream = new(data, false);
DecoderOptions strict = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict };
Assert.Throws<InvalidImageContentException>(() => AniDecoder.Instance.Decode<Rgba32>(strict, strictStream));
}
}

156
tests/ImageSharp.Tests/Formats/Ani/AniEncoderTests.cs

@ -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);
}
}

31
tests/ImageSharp.Tests/Formats/Ani/AniMetadataTests.cs

@ -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);
}
}

40
tests/ImageSharp.Tests/Formats/Icon/Cur/CurDecoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Icon;
@ -13,6 +14,19 @@ namespace SixLabors.ImageSharp.Tests.Formats.Icon.Cur;
[ValidateDisposedMemoryAllocations]
public class CurDecoderTests
{
[Fact]
public void CurFormat_HasCorrectName()
=> Assert.Equal("CUR", CurFormat.Instance.Name);
[Fact]
public void CurDetector_RejectsIco()
{
TestFile file = TestFile.Create(TestImages.Ico.Flutter);
CurImageFormatDetector detector = new();
Assert.False(detector.TryDetectFormat(file.Bytes, out _));
}
[Theory]
[WithFile(WindowsMouse, PixelTypes.Rgba32)]
public void CurDecoder_Decode(TestImageProvider<Rgba32> provider)
@ -38,4 +52,30 @@ public class CurDecoderTests
Assert.Equal(IconFrameCompression.Bmp, meta.Compression);
Assert.Equal(BmpBitsPerPixel.Bit32, meta.BmpBitsPerPixel);
}
[Fact]
public void CurFrameMetadata_DeepClonePreservesColorTable()
{
Color[] colors = [Color.Red, Color.Green];
CurFrameMetadata metadata = new() { ColorTable = colors };
CurFrameMetadata clone = metadata.DeepClone();
colors[0] = Color.Blue;
Assert.Equal(Color.Red, clone.ColorTable.Value.Span[0]);
Assert.Equal(Color.Green, clone.ColorTable.Value.Span[1]);
}
[Fact]
public void CurFrameMetadata_ScalesZeroEncodingDimensionsFrom256()
{
using Image<Rgba32> source = new(256, 256);
using Image<Rgba32> destination = new(128, 128);
CurFrameMetadata metadata = new() { EncodingWidth = 0, EncodingHeight = 0 };
metadata.AfterFrameApply(source.Frames.RootFrame, destination.Frames.RootFrame, Matrix4x4.Identity);
Assert.Equal((byte)128, metadata.EncodingWidth);
Assert.Equal((byte)128, metadata.EncodingHeight);
}
}

36
tests/ImageSharp.Tests/Formats/Icon/Cur/CurEncoderTests.cs

@ -2,9 +2,12 @@
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Cur;
using SixLabors.ImageSharp.Formats.Ico;
using SixLabors.ImageSharp.Formats.Icon;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using static SixLabors.ImageSharp.Tests.TestImages.Cur;
using static SixLabors.ImageSharp.Tests.TestImages.Ico;
@ -124,4 +127,37 @@ public class CurEncoderTests
}
});
}
[Fact]
public void Encode_UsesBitmapDepthInsteadOfHotspotForQuantizerSelection()
{
using Image<Rgba32> image = new(32, 1);
for (int x = 0; x < image.Width; x++)
{
image[x, 0] = new Rgba32((byte)(x * 8), (byte)(255 - (x * 8)), (byte)(x * 4));
}
CurFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetCurMetadata();
metadata.Compression = IconFrameCompression.Bmp;
metadata.BmpBitsPerPixel = BmpBitsPerPixel.Bit8;
metadata.HotspotY = 32;
CurEncoder encoder = new()
{
Quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = 2 })
};
using MemoryStream stream = new();
image.Save(stream, encoder);
stream.Position = 0;
using Image<Rgba32> decoded = Image.Load<Rgba32>(stream);
HashSet<Rgba32> colors = [];
for (int x = 0; x < decoded.Width; x++)
{
colors.Add(decoded[x, 0]);
}
Assert.InRange(colors.Count, 1, 2);
}
}

48
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers.Binary;
using System.Numerics;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Ico;
@ -14,6 +16,15 @@ namespace SixLabors.ImageSharp.Tests.Formats.Icon.Ico;
[ValidateDisposedMemoryAllocations]
public class IcoDecoderTests
{
[Fact]
public void IcoDetector_RejectsCur()
{
TestFile file = TestFile.Create(TestImages.Cur.WindowsMouse);
IcoImageFormatDetector detector = new();
Assert.False(detector.TryDetectFormat(file.Bytes, out _));
}
[Theory]
[WithFile(Flutter, PixelTypes.Rgba32)]
public void IcoDecoder_Decode(TestImageProvider<Rgba32> provider)
@ -306,6 +317,43 @@ public class IcoDecoderTests
Assert.Single(imageInfo.FrameMetadataCollection);
}
[Fact]
public void TruncatedEntry_FollowsImageDataIntegrityHandling()
{
byte[] data = TestFile.Create(Flutter).Bytes.ToArray();
ushort entryCount = BinaryPrimitives.ReadUInt16LittleEndian(data.AsSpan(4));
Assert.True(entryCount > 1);
// Limit the first resource below the PNG signature length. A bounded child decoder must not read into following data.
BinaryPrimitives.WriteUInt32LittleEndian(data.AsSpan(IconDir.Size + 8), 4);
using MemoryStream defaultStream = new(data, false);
Assert.Throws<InvalidImageContentException>(() => IcoDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, defaultStream));
DecoderOptions ignore = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData };
using MemoryStream decodeStream = new(data, false);
using Image<Rgba32> image = IcoDecoder.Instance.Decode<Rgba32>(ignore, decodeStream);
Assert.Equal(entryCount - 1, image.Frames.Count);
using MemoryStream identifyStream = new(data, false);
ImageInfo info = IcoDecoder.Instance.Identify(ignore, identifyStream);
Assert.Equal(entryCount - 1, info.FrameMetadataCollection.Count);
}
[Fact]
public void IcoFrameMetadata_ScalesZeroEncodingDimensionsFrom256()
{
using Image<Rgba32> source = new(256, 256);
using Image<Rgba32> destination = new(128, 128);
IcoFrameMetadata metadata = new() { EncodingWidth = 0, EncodingHeight = 0 };
metadata.AfterFrameApply(source.Frames.RootFrame, destination.Frames.RootFrame, Matrix4x4.Identity);
Assert.Equal((byte)128, metadata.EncodingWidth);
Assert.Equal((byte)128, metadata.EncodingHeight);
}
[Theory]
[WithFile(MultiSizeMultiBitsA, PixelTypes.Rgba32)]
[WithFile(MultiSizeMultiBitsB, PixelTypes.Rgba32)]

3
tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs

@ -3,6 +3,7 @@
using Moq;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Ani;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
@ -30,6 +31,7 @@ public class ImageFormatManagerTests
[Fact]
public void IfAutoLoadWellKnownFormatsIsTrueAllFormatsAreLoaded()
{
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<AniEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<PbmEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<PngEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<BmpEncoder>().Count());
@ -39,6 +41,7 @@ public class ImageFormatManagerTests
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<TiffEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType<WebpEncoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<AniDecoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<PbmDecoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<PngDecoder>().Count());
Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType<BmpDecoder>().Count());

Loading…
Cancel
Save