mirror of https://github.com/SixLabors/ImageSharp
61 changed files with 2210 additions and 491 deletions
@ -0,0 +1,72 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Extension methods for the <see cref="BufferedReadStream"/> type.
|
||||
|
/// </summary>
|
||||
|
internal static class BufferedReadStreamExtensions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Determines whether the complete read range is contained in the stream.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing the data.</param>
|
||||
|
/// <param name="offset">The absolute start of the range.</param>
|
||||
|
/// <param name="length">The number of bytes in the range.</param>
|
||||
|
/// <returns>Whether the range is contained in the stream.</returns>
|
||||
|
public static bool IsReadRangeValid(this BufferedReadStream stream, long offset, ulong length) |
||||
|
{ |
||||
|
// Compare the offset first so subtraction cannot underflow, and avoid
|
||||
|
// adding an untrusted length to the offset where it could wrap around.
|
||||
|
ulong streamLength = (ulong)stream.Length; |
||||
|
return (ulong)offset <= streamLength && length <= streamLength - (ulong)offset; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets a buffer length when the complete read fits in both the stream and an integer-sized buffer.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream containing the data.</param>
|
||||
|
/// <param name="length">The declared length in bytes.</param>
|
||||
|
/// <param name="bufferLength">The validated length, or zero when the range is invalid.</param>
|
||||
|
/// <returns>Whether the complete read is valid.</returns>
|
||||
|
public static bool TryGetReadLength(this BufferedReadStream stream, ulong length, out int bufferLength) |
||||
|
{ |
||||
|
if (length > int.MaxValue || !stream.IsReadRangeValid(stream.Position, length)) |
||||
|
{ |
||||
|
bufferLength = 0; |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
bufferLength = (int)length; |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reads data from the stream into a slice of the provided buffer.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream.</param>
|
||||
|
/// <param name="buffer">The buffer.</param>
|
||||
|
/// <param name="offset">The offset within the buffer where bytes are read into.</param>
|
||||
|
/// <param name="count">The number of bytes, if available, to read.</param>
|
||||
|
/// <returns>The actual number of bytes read.</returns>
|
||||
|
public static int Read(this BufferedReadStream stream, Span<byte> buffer, int offset, int count) |
||||
|
=> stream.Read(buffer.Slice(offset, count)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Advances the stream by the specified number of bytes. Nonpositive counts are ignored.
|
||||
|
/// </summary>
|
||||
|
/// <param name="stream">The stream.</param>
|
||||
|
/// <param name="count">The number of bytes to skip.</param>
|
||||
|
public static void Skip(this BufferedReadStream stream, int count) |
||||
|
{ |
||||
|
if (count > 0) |
||||
|
{ |
||||
|
// BufferedReadStream is always seekable; its position setter preserves
|
||||
|
// buffered data when the destination is inside the current buffer.
|
||||
|
stream.Position += count; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,60 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.IO; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Common; |
||||
|
|
||||
|
public class BufferedReadStreamExtensionsTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData(0L, 8UL, true)] |
||||
|
[InlineData(8L, 0UL, true)] |
||||
|
[InlineData(7L, 2UL, false)] |
||||
|
[InlineData(9L, 0UL, false)] |
||||
|
[InlineData(-1L, 1UL, false)] |
||||
|
[InlineData(long.MaxValue, ulong.MaxValue, false)] |
||||
|
[InlineData(0L, ulong.MaxValue, false)] |
||||
|
public void IsReadRangeValid_ChecksCompleteExtent(long offset, ulong length, bool expected) |
||||
|
{ |
||||
|
using MemoryStream input = new(new byte[8]); |
||||
|
using BufferedReadStream stream = new(Configuration.Default, input); |
||||
|
|
||||
|
Assert.Equal(expected, stream.IsReadRangeValid(offset, length)); |
||||
|
Assert.Equal(0, stream.Position); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0UL, true, 0)] |
||||
|
[InlineData(6UL, true, 6)] |
||||
|
[InlineData(7UL, false, 0)] |
||||
|
[InlineData(1073741824UL, false, 0)] |
||||
|
[InlineData(4294967294UL, false, 0)] |
||||
|
[InlineData(4294967296UL, false, 0)] |
||||
|
[InlineData(ulong.MaxValue, false, 0)] |
||||
|
public void TryGetReadLength_ReturnsResultWithoutMovingStream(ulong length, bool expected, int expectedLength) |
||||
|
{ |
||||
|
using MemoryStream input = new(new byte[8]); |
||||
|
using BufferedReadStream stream = new(Configuration.Default, input); |
||||
|
stream.Position = 2; |
||||
|
|
||||
|
Assert.Equal(expected, stream.TryGetReadLength(length, out int bufferLength)); |
||||
|
Assert.Equal(expectedLength, bufferLength); |
||||
|
Assert.Equal(2, stream.Position); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(0)] |
||||
|
[InlineData(-1)] |
||||
|
public void Skip_CountZeroOrLower_PositionNotChanged(int count) |
||||
|
{ |
||||
|
using MemoryStream input = new(new byte[8]); |
||||
|
using BufferedReadStream stream = new(Configuration.Default, input); |
||||
|
stream.Position = 4; |
||||
|
|
||||
|
stream.Skip(count); |
||||
|
|
||||
|
Assert.Equal(4, stream.Position); |
||||
|
Assert.Equal(0, stream.ReadByte()); |
||||
|
} |
||||
|
} |
||||
@ -1,111 +0,0 @@ |
|||||
// Copyright (c) Six Labors.
|
|
||||
// Licensed under the Six Labors Split License.
|
|
||||
|
|
||||
namespace SixLabors.ImageSharp.Tests.Common; |
|
||||
|
|
||||
public class StreamExtensionsTests |
|
||||
{ |
|
||||
[Theory] |
|
||||
[InlineData(0)] |
|
||||
[InlineData(-1)] |
|
||||
public void Skip_CountZeroOrLower_PositionNotChanged(int count) |
|
||||
{ |
|
||||
using (MemoryStream memStream = new(5)) |
|
||||
{ |
|
||||
memStream.Position = 4; |
|
||||
memStream.Skip(count); |
|
||||
|
|
||||
Assert.Equal(4, memStream.Position); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Skip_SeekableStream_SeekIsCalled() |
|
||||
{ |
|
||||
using (SeekableStream seekableStream = new(4)) |
|
||||
{ |
|
||||
seekableStream.Skip(4); |
|
||||
|
|
||||
Assert.Equal(4, seekableStream.Offset); |
|
||||
Assert.Equal(SeekOrigin.Current, seekableStream.Loc); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Skip_NonSeekableStream_BytesAreRead() |
|
||||
{ |
|
||||
using (NonSeekableStream nonSeekableStream = new()) |
|
||||
{ |
|
||||
nonSeekableStream.Skip(5); |
|
||||
|
|
||||
Assert.Equal(3, nonSeekableStream.Counts.Count); |
|
||||
|
|
||||
Assert.Equal(5, nonSeekableStream.Counts[0]); |
|
||||
Assert.Equal(3, nonSeekableStream.Counts[1]); |
|
||||
Assert.Equal(1, nonSeekableStream.Counts[2]); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public void Skip_EofStream_NoExceptionIsThrown() |
|
||||
{ |
|
||||
using (EofStream eofStream = new(7)) |
|
||||
{ |
|
||||
eofStream.Skip(7); |
|
||||
|
|
||||
Assert.Equal(0, eofStream.Position); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class SeekableStream : MemoryStream |
|
||||
{ |
|
||||
public long Offset; |
|
||||
public SeekOrigin Loc; |
|
||||
|
|
||||
public SeekableStream(int capacity) |
|
||||
: base(capacity) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public override long Seek(long offset, SeekOrigin loc) |
|
||||
{ |
|
||||
this.Offset = offset; |
|
||||
this.Loc = loc; |
|
||||
return base.Seek(offset, loc); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class NonSeekableStream : MemoryStream |
|
||||
{ |
|
||||
public override bool CanSeek => false; |
|
||||
|
|
||||
public List<int> Counts = []; |
|
||||
|
|
||||
public NonSeekableStream() |
|
||||
: base(4) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public override int Read(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
this.Counts.Add(count); |
|
||||
|
|
||||
return Math.Min(2, count); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private class EofStream : MemoryStream |
|
||||
{ |
|
||||
public override bool CanSeek => false; |
|
||||
|
|
||||
public EofStream(int capacity) |
|
||||
: base(capacity) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public override int Read(byte[] buffer, int offset, int count) |
|
||||
{ |
|
||||
return 0; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,248 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
using System.IO.Compression; |
||||
|
using System.Numerics; |
||||
|
using System.Text; |
||||
|
using SixLabors.ImageSharp.Formats; |
||||
|
using SixLabors.ImageSharp.Formats.Exr.Constants; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
using SixLabors.ImageSharp.Tests.Memory; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Exr; |
||||
|
|
||||
|
[Trait("Format", "Exr")] |
||||
|
[ValidateDisposedMemoryAllocations] |
||||
|
public class ExrZipDecoderTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Incomplete and oversized blocks are rejected unless image-data recovery is enabled.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The inflated payload length.</param>
|
||||
|
/// <param name="integrity">The image-data integrity policy.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(0, SegmentIntegrityHandling.Strict)] |
||||
|
[InlineData(8, SegmentIntegrityHandling.Strict)] |
||||
|
[InlineData(1025, SegmentIntegrityHandling.Strict)] |
||||
|
[InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] |
||||
|
[InlineData(8, SegmentIntegrityHandling.IgnoreAncillary)] |
||||
|
[InlineData(1025, SegmentIntegrityHandling.IgnoreAncillary)] |
||||
|
public void Decode_InvalidInflatedBlock_Throws(int length, SegmentIntegrityHandling integrity) |
||||
|
{ |
||||
|
byte[] data = BuildExr(ZlibCompress(new byte[length]), ExrPixelType.Float, 2, 0); |
||||
|
DecoderOptions options = new() { SegmentIntegrityHandling = integrity }; |
||||
|
|
||||
|
Assert.Throws<InvalidImageContentException>(() => Image.Load<RgbaVector>(options, data)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Recovering an invalid image-data block must not expose partially decoded or pooled bytes.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixelType">The stored sample type.</param>
|
||||
|
/// <param name="length">The inflated payload length.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(ExrPixelType.Half, 0)] |
||||
|
[InlineData(ExrPixelType.Half, 8)] |
||||
|
[InlineData(ExrPixelType.Half, 1025)] |
||||
|
[InlineData(ExrPixelType.Float, 0)] |
||||
|
[InlineData(ExrPixelType.Float, 8)] |
||||
|
[InlineData(ExrPixelType.Float, 1025)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 0)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 8)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 1025)] |
||||
|
public void Decode_InvalidInflatedBlock_IgnoreImageData_ClearsPixels(ExrPixelType pixelType, int length) |
||||
|
{ |
||||
|
byte[] data = BuildExr(ZlibCompress(new byte[length]), pixelType, 2, 0); |
||||
|
Configuration configuration = Configuration.Default.Clone(); |
||||
|
configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); |
||||
|
DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; |
||||
|
|
||||
|
using Image<RgbaVector> image = Image.Load<RgbaVector>(options, data); |
||||
|
Assert.Equal(new Size(256, 1), image.Size); |
||||
|
|
||||
|
for (int x = 0; x < image.Width; x++) |
||||
|
{ |
||||
|
Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Missing or truncated zlib headers obey the image-data integrity policy.
|
||||
|
/// </summary>
|
||||
|
/// <param name="length">The number of available zlib header bytes.</param>
|
||||
|
/// <param name="integrity">The image-data integrity policy.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(0, SegmentIntegrityHandling.Strict)] |
||||
|
[InlineData(1, SegmentIntegrityHandling.Strict)] |
||||
|
[InlineData(0, SegmentIntegrityHandling.IgnoreAncillary)] |
||||
|
[InlineData(1, SegmentIntegrityHandling.IgnoreAncillary)] |
||||
|
[InlineData(0, SegmentIntegrityHandling.IgnoreImageData)] |
||||
|
[InlineData(1, SegmentIntegrityHandling.IgnoreImageData)] |
||||
|
public void Decode_IncompleteZlibHeader_RespectsIntegrityHandling(int length, SegmentIntegrityHandling integrity) |
||||
|
{ |
||||
|
byte[] header = [0x78, 0x9C]; |
||||
|
byte[] data = BuildExr(header[..length], ExrPixelType.Float, 2, 0); |
||||
|
Configuration configuration = Configuration.Default.Clone(); |
||||
|
configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); |
||||
|
DecoderOptions options = new() { Configuration = configuration, SegmentIntegrityHandling = integrity }; |
||||
|
|
||||
|
if (integrity == SegmentIntegrityHandling.IgnoreImageData) |
||||
|
{ |
||||
|
using Image<RgbaVector> image = Image.Load<RgbaVector>(options, data); |
||||
|
Assert.Equal(new Size(256, 1), image.Size); |
||||
|
|
||||
|
for (int x = 0; x < image.Width; x++) |
||||
|
{ |
||||
|
Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Assert.Throws<InvalidImageContentException>(() => Image.Load<RgbaVector>(options, data)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Missing color channels must not inherit the allocator's previous contents.
|
||||
|
/// </summary>
|
||||
|
/// <param name="pixelType">The stored sample type.</param>
|
||||
|
/// <param name="compression">The ZIP compression code.</param>
|
||||
|
/// <param name="yMin">The data window's first row coordinate.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(ExrPixelType.Half, 2, 0)] |
||||
|
[InlineData(ExrPixelType.Float, 2, 0)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 2, 0)] |
||||
|
[InlineData(ExrPixelType.Half, 3, 0)] |
||||
|
[InlineData(ExrPixelType.Float, 3, 0)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 3, 0)] |
||||
|
[InlineData(ExrPixelType.Half, 3, -10)] |
||||
|
[InlineData(ExrPixelType.Float, 3, -10)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 3, -10)] |
||||
|
[InlineData(ExrPixelType.Half, 3, 10)] |
||||
|
[InlineData(ExrPixelType.Float, 3, 10)] |
||||
|
[InlineData(ExrPixelType.UnsignedInt, 3, 10)] |
||||
|
public void Decode_SingleRedChannel_InitializesMissingColorChannels(ExrPixelType pixelType, byte compression, int yMin) |
||||
|
{ |
||||
|
byte[] predicted = new byte[256 * (pixelType == ExrPixelType.Half ? 2 : 4)]; |
||||
|
|
||||
|
// A zero first byte followed by 128-valued differences reconstructs an all-zero sample plane.
|
||||
|
predicted.AsSpan(1).Fill(128); |
||||
|
byte[] data = BuildExr(ZlibCompress(predicted), pixelType, compression, yMin); |
||||
|
Configuration configuration = Configuration.Default.Clone(); |
||||
|
configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); |
||||
|
DecoderOptions options = new() { Configuration = configuration }; |
||||
|
|
||||
|
using Image<RgbaVector> image = Image.Load<RgbaVector>(options, data); |
||||
|
Assert.Equal(new Size(256, 1), image.Size); |
||||
|
|
||||
|
for (int x = 0; x < image.Width; x++) |
||||
|
{ |
||||
|
Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Compresses the predictor bytes for a scanline block.
|
||||
|
/// </summary>
|
||||
|
/// <param name="data">The predictor bytes.</param>
|
||||
|
/// <returns>The zlib stream.</returns>
|
||||
|
private static byte[] ZlibCompress(byte[] data) |
||||
|
{ |
||||
|
if (data.Length == 0) |
||||
|
{ |
||||
|
// An empty write produces no output on some runtimes. Use a complete zlib stream
|
||||
|
// containing an empty final DEFLATE block and Adler-32 checksum instead.
|
||||
|
return [0x78, 0x9C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x01]; |
||||
|
} |
||||
|
|
||||
|
using MemoryStream output = new(); |
||||
|
using (ZLibStream zlib = new(output, CompressionLevel.Optimal, leaveOpen: true)) |
||||
|
{ |
||||
|
zlib.Write(data); |
||||
|
} |
||||
|
|
||||
|
return output.ToArray(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Builds a single-row EXR containing only the red channel.
|
||||
|
/// </summary>
|
||||
|
/// <param name="compressed">The compressed scanline bytes.</param>
|
||||
|
/// <param name="pixelType">The stored sample type.</param>
|
||||
|
/// <param name="compression">The ZIP compression code.</param>
|
||||
|
/// <param name="yMin">The data window's first row coordinate.</param>
|
||||
|
/// <returns>The encoded image.</returns>
|
||||
|
private static byte[] BuildExr(byte[] compressed, ExrPixelType pixelType, byte compression, int yMin) |
||||
|
{ |
||||
|
const int width = 256; |
||||
|
const int height = 1; |
||||
|
|
||||
|
using MemoryStream output = new(); |
||||
|
using BinaryWriter writer = new(output); |
||||
|
|
||||
|
writer.Write(new byte[] { 0x76, 0x2F, 0x31, 0x01 }); |
||||
|
writer.Write((byte)2); |
||||
|
writer.Write(new byte[] { 0, 0, 0 }); |
||||
|
|
||||
|
using (MemoryStream channelStream = new()) |
||||
|
using (BinaryWriter channelWriter = new(channelStream)) |
||||
|
{ |
||||
|
WriteString(channelWriter, "R"); |
||||
|
channelWriter.Write((int)pixelType); |
||||
|
channelWriter.Write((byte)0); |
||||
|
channelWriter.Write(new byte[] { 0, 0, 0 }); |
||||
|
channelWriter.Write(1); |
||||
|
channelWriter.Write(1); |
||||
|
channelWriter.Write((byte)0); |
||||
|
|
||||
|
WriteAttribute(writer, "channels", "chlist", channelStream.ToArray()); |
||||
|
} |
||||
|
|
||||
|
WriteAttribute(writer, "compression", "compression", [compression]); |
||||
|
|
||||
|
using (MemoryStream boxStream = new()) |
||||
|
using (BinaryWriter boxWriter = new(boxStream)) |
||||
|
{ |
||||
|
boxWriter.Write(0); |
||||
|
boxWriter.Write(yMin); |
||||
|
boxWriter.Write(width - 1); |
||||
|
boxWriter.Write(yMin + height - 1); |
||||
|
|
||||
|
byte[] box = boxStream.ToArray(); |
||||
|
WriteAttribute(writer, "dataWindow", "box2i", box); |
||||
|
WriteAttribute(writer, "displayWindow", "box2i", box); |
||||
|
} |
||||
|
|
||||
|
WriteAttribute(writer, "lineOrder", "lineOrder", [0]); |
||||
|
|
||||
|
byte[] one = new byte[4]; |
||||
|
BinaryPrimitives.WriteSingleLittleEndian(one, 1F); |
||||
|
WriteAttribute(writer, "pixelAspectRatio", "float", one); |
||||
|
WriteAttribute(writer, "screenWindowCenter", "v2f", new byte[8]); |
||||
|
WriteAttribute(writer, "screenWindowWidth", "float", one); |
||||
|
writer.Write((byte)0); |
||||
|
|
||||
|
long chunkStart = output.Position + sizeof(ulong); |
||||
|
writer.Write((ulong)chunkStart); |
||||
|
writer.Write(yMin); |
||||
|
writer.Write((uint)compressed.Length); |
||||
|
writer.Write(compressed); |
||||
|
|
||||
|
return output.ToArray(); |
||||
|
} |
||||
|
|
||||
|
private static void WriteString(BinaryWriter writer, string value) |
||||
|
{ |
||||
|
writer.Write(Encoding.ASCII.GetBytes(value)); |
||||
|
writer.Write((byte)0); |
||||
|
} |
||||
|
|
||||
|
private static void WriteAttribute(BinaryWriter writer, string name, string type, byte[] value) |
||||
|
{ |
||||
|
WriteString(writer, name); |
||||
|
WriteString(writer, type); |
||||
|
writer.Write(value.Length); |
||||
|
writer.Write(value); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats; |
||||
|
|
||||
|
public class InvalidImageDimensionsTests |
||||
|
{ |
||||
|
[Theory] |
||||
|
[InlineData("Qk1GAAAAAAAAADYAAAAoAAAAAgACAAAAAAABABgAAAAAABAAAAATCwAAEwsAAAAAAAAAAAAAAAD/AP8AAAAAAP8A/wAAAA==")] |
||||
|
[InlineData("R0lGODdhAgIAAIEAAAD/AP8AAAAA/wAAACwAAAQAAgACAAAIBwADABAQICAAOw==")] |
||||
|
[InlineData("AAACAAAAAAAAAAAAAgDCsRgAAgAAAP8A/wD/AAAA//8=")] |
||||
|
public void Load_WithNonPositiveDimensions_ThrowsInvalidImageContentException(string encodedData) |
||||
|
{ |
||||
|
byte[] data = Convert.FromBase64String(encodedData); |
||||
|
|
||||
|
Assert.Throws<InvalidImageContentException>(() => Image.Load(data)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using SixLabors.ImageSharp.Formats.Png; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Png; |
||||
|
|
||||
|
[Trait("Format", "Png")] |
||||
|
public class PngDecoderCoreTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void CalculateScanlineLength_WithLargeGrayscaleWidth_ReturnsExpectedLength() |
||||
|
{ |
||||
|
int length = PngDecoderCore.CalculateScanlineLength(536_870_913, 8, 1); |
||||
|
|
||||
|
Assert.Equal(536_870_913, length); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CalculateScanlineLength_WithLargeRgbaWidth_ReturnsExpectedLength() |
||||
|
{ |
||||
|
int length = PngDecoderCore.CalculateScanlineLength(33_554_432, 16, 8); |
||||
|
|
||||
|
Assert.Equal(268_435_456, length); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Buffers.Binary; |
||||
|
using System.Text; |
||||
|
using SixLabors.ImageSharp.Formats; |
||||
|
using SixLabors.ImageSharp.Formats.Png; |
||||
|
using SixLabors.ImageSharp.Metadata.Profiles.Icc; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.Formats.Png; |
||||
|
|
||||
|
public partial class PngDecoderTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Decode_IccLutExceedsVectorChannelCount_Throws() |
||||
|
{ |
||||
|
byte[] profileData = BuildLut16Profile(3, 15, 2, 2, 2); |
||||
|
byte[] pngData = BuildPng(profileData); |
||||
|
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert }; |
||||
|
|
||||
|
Assert.Throws<InvalidIccProfileException>(() => Image.Load(options, pngData)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Three-channel LUT conversion remains supported.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void Decode_IccLutWithSupportedChannelCount_ConvertsPixels() |
||||
|
{ |
||||
|
byte[] pngData = BuildPng(BuildLut16Profile(3, 3, 2, 2, 2)); |
||||
|
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Convert }; |
||||
|
|
||||
|
using Image<Rgb24> image = Image.Load<Rgb24>(options, pngData); |
||||
|
Assert.Equal(new Size(16, 16), image.Size); |
||||
|
|
||||
|
// Every CLUT node contains a nonzero XYZ value, even though the encoded pixels are black.
|
||||
|
Assert.NotEqual(default(Rgb24), image[0, 0]); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preserving a profile does not impose the converter's four-component storage limit on the parser.
|
||||
|
/// </summary>
|
||||
|
/// <param name="outputChannels">The number of output channels in the LUT.</param>
|
||||
|
[Theory] |
||||
|
[InlineData(3)] |
||||
|
[InlineData(15)] |
||||
|
public void Decode_IccLut_Preserve_RetainsChannels(int outputChannels) |
||||
|
{ |
||||
|
byte[] pngData = BuildPng(BuildLut16Profile(3, outputChannels, 2, 2, 2)); |
||||
|
DecoderOptions options = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; |
||||
|
|
||||
|
using Image<Rgb24> image = Image.Load<Rgb24>(options, pngData); |
||||
|
IccLut16TagDataEntry entry = Assert.IsType<IccLut16TagDataEntry>(Assert.Single(image.Metadata.IccProfile.Entries)); |
||||
|
Assert.Equal(outputChannels, entry.OutputValues.Length); |
||||
|
Assert.Equal(default(Rgb24), image[0, 0]); |
||||
|
} |
||||
|
|
||||
|
private static byte[] BuildLut16Profile(int inputChannels, int outputChannels, int clutPoints, int inputTableLength, int outputTableLength) |
||||
|
{ |
||||
|
using MemoryStream stream = new(); |
||||
|
|
||||
|
byte[] header = new byte[128]; |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(8), 0x04300000U); |
||||
|
Encoding.ASCII.GetBytes("mntr").CopyTo(header, 12); |
||||
|
Encoding.ASCII.GetBytes("RGB ").CopyTo(header, 16); |
||||
|
Encoding.ASCII.GetBytes("XYZ ").CopyTo(header, 20); |
||||
|
stream.Write(header); |
||||
|
|
||||
|
WriteUInt32(1); |
||||
|
stream.Write(Encoding.ASCII.GetBytes("A2B0")); |
||||
|
long offsetPosition = stream.Position; |
||||
|
WriteUInt32(0); |
||||
|
long sizePosition = stream.Position; |
||||
|
WriteUInt32(0); |
||||
|
|
||||
|
long tagStart = stream.Position; |
||||
|
stream.Write(Encoding.ASCII.GetBytes("mft2")); |
||||
|
WriteUInt32(0); |
||||
|
stream.WriteByte((byte)inputChannels); |
||||
|
stream.WriteByte((byte)outputChannels); |
||||
|
stream.WriteByte((byte)clutPoints); |
||||
|
stream.WriteByte(0); |
||||
|
|
||||
|
for (int y = 0; y < 3; y++) |
||||
|
{ |
||||
|
for (int x = 0; x < 3; x++) |
||||
|
{ |
||||
|
WriteFix16(x == y ? 1D : 0D); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
WriteUInt16((ushort)inputTableLength); |
||||
|
WriteUInt16((ushort)outputTableLength); |
||||
|
|
||||
|
for (int channel = 0; channel < inputChannels; channel++) |
||||
|
{ |
||||
|
for (int i = 0; i < inputTableLength; i++) |
||||
|
{ |
||||
|
WriteUInt16((ushort)(i == 0 ? 0 : ushort.MaxValue)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int clutLength = (int)Math.Pow(clutPoints, inputChannels); |
||||
|
for (int i = 0; i < clutLength; i++) |
||||
|
{ |
||||
|
for (int channel = 0; channel < outputChannels; channel++) |
||||
|
{ |
||||
|
WriteUInt16(0x8000); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
for (int channel = 0; channel < outputChannels; channel++) |
||||
|
{ |
||||
|
for (int i = 0; i < outputTableLength; i++) |
||||
|
{ |
||||
|
WriteUInt16((ushort)(i == 0 ? 0 : ushort.MaxValue)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
long tagEnd = stream.Position; |
||||
|
byte[] result = stream.ToArray(); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)offsetPosition), (uint)tagStart); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(result.AsSpan((int)sizePosition), (uint)(tagEnd - tagStart)); |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(result, (uint)result.Length); |
||||
|
return result; |
||||
|
|
||||
|
void WriteUInt32(uint value) |
||||
|
{ |
||||
|
Span<byte> buffer = stackalloc byte[4]; |
||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer, value); |
||||
|
stream.Write(buffer); |
||||
|
} |
||||
|
|
||||
|
void WriteUInt16(ushort value) |
||||
|
{ |
||||
|
Span<byte> buffer = stackalloc byte[2]; |
||||
|
BinaryPrimitives.WriteUInt16BigEndian(buffer, value); |
||||
|
stream.Write(buffer); |
||||
|
} |
||||
|
|
||||
|
void WriteFix16(double value) |
||||
|
{ |
||||
|
int rawValue = (int)Math.Round(value * 65536D); |
||||
|
WriteUInt32(unchecked((uint)rawValue)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static byte[] BuildPng(byte[] profileData) |
||||
|
{ |
||||
|
using Image<Rgb24> image = new(16, 16); |
||||
|
image.Metadata.IccProfile = new IccProfile(profileData); |
||||
|
|
||||
|
using MemoryStream stream = new(); |
||||
|
image.SaveAsPng(stream); |
||||
|
return stream.ToArray(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
// Copyright (c) Six Labors.
|
||||
|
// Licensed under the Six Labors Split License.
|
||||
|
|
||||
|
using System.Numerics; |
||||
|
using SixLabors.ImageSharp.PixelFormats; |
||||
|
|
||||
|
namespace SixLabors.ImageSharp.Tests.PixelFormats; |
||||
|
|
||||
|
[Trait("Category", "PixelFormats")] |
||||
|
public class FloatingPointPixelNormalizationTests |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// HalfSingle normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfSingle_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfSingle>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// HalfVector2 normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfVector2_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector2>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// HalfVector4 normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfVector4_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// HalfVector4P normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfVector4P_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<HalfVector4P>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// RgbaVector normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void RgbaVector_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaVector>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// RgbaHalf normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void RgbaHalf_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalf>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// RgbaHalfP normalizes scaled input identically in scalar and bulk conversions.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void RgbaHalfP_ScaledInputIsNormalized() => AssertScaledInputIsNormalized<RgbaHalfP>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Raw half storage preserves IEEE special values while its scaled representation remains finite.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfVector4_NativeSpecialValuesHaveNormalizedScaledOutput() => AssertNativeSpecialValuesHaveNormalizedScaledOutput(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Associated half-vector conversion uses the stored alpha ratio before normalizing RGB.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void HalfVector4P_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<HalfVector4P>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Associated half-RGBA conversion uses the stored alpha ratio before normalizing RGB.
|
||||
|
/// </summary>
|
||||
|
[Fact] |
||||
|
public void RgbaHalfP_AssociatedScaledInputIsNormalized() => AssertAssociatedScaledInputIsNormalized<RgbaHalfP>(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks saturation and NaN handling without deriving expectations from the invalid-input path.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The destination pixel format.</typeparam>
|
||||
|
private static void AssertScaledInputIsNormalized<TPixel>() |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Vector4[] inputs = |
||||
|
[ |
||||
|
new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), |
||||
|
new(2F, -2F, .5F, 1F), |
||||
|
new(.25F, .5F, .75F, .5F), |
||||
|
new(.25F, .5F, .75F, float.NaN), |
||||
|
new(.25F, .5F, .75F, float.PositiveInfinity) |
||||
|
]; |
||||
|
|
||||
|
Vector4[] normalized = |
||||
|
[ |
||||
|
new(1F, 0F, 0F, 1F), |
||||
|
new(1F, 0F, .5F, 1F), |
||||
|
new(.25F, .5F, .75F, .5F), |
||||
|
new(.25F, .5F, .75F, 0F), |
||||
|
new(.25F, .5F, .75F, 1F) |
||||
|
]; |
||||
|
|
||||
|
// Seventeen pixels exercise wide registers and the narrower remainder paths.
|
||||
|
Vector4[] source = new Vector4[17]; |
||||
|
TPixel[] expected = new TPixel[source.Length]; |
||||
|
TPixel[] actual = new TPixel[source.Length]; |
||||
|
|
||||
|
for (int i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
int sample = i % inputs.Length; |
||||
|
source[i] = inputs[sample]; |
||||
|
expected[i] = TPixel.FromUnassociatedScaledVector4(normalized[sample]); |
||||
|
Assert.Equal(expected[i], TPixel.FromUnassociatedScaledVector4(source[i])); |
||||
|
} |
||||
|
|
||||
|
// Associated formats otherwise interpret the vectors using their native alpha representation.
|
||||
|
PixelOperations<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); |
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks associated input against finite control values with the same represented color.
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="TPixel">The associated destination pixel format.</typeparam>
|
||||
|
private static void AssertAssociatedScaledInputIsNormalized<TPixel>() |
||||
|
where TPixel : unmanaged, IPixel<TPixel> |
||||
|
{ |
||||
|
Vector4[] inputs = |
||||
|
[ |
||||
|
new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 1F), |
||||
|
new(1F, .5F, 1.5F, 2F), |
||||
|
new(.125F, .25F, .375F, .5F), |
||||
|
new(.25F, .5F, .75F, float.NaN), |
||||
|
new(.25F, .5F, .75F, float.PositiveInfinity) |
||||
|
]; |
||||
|
|
||||
|
Vector4[] normalized = |
||||
|
[ |
||||
|
new(1F, 0F, 0F, 1F), |
||||
|
new(.5F, .25F, .75F, 1F), |
||||
|
new(.125F, .25F, .375F, .5F), |
||||
|
Vector4.Zero, |
||||
|
new(0F, 0F, 0F, 1F) |
||||
|
]; |
||||
|
|
||||
|
Vector4[] source = new Vector4[17]; |
||||
|
TPixel[] expected = new TPixel[source.Length]; |
||||
|
TPixel[] actual = new TPixel[source.Length]; |
||||
|
|
||||
|
for (int i = 0; i < source.Length; i++) |
||||
|
{ |
||||
|
int sample = i % inputs.Length; |
||||
|
source[i] = inputs[sample]; |
||||
|
expected[i] = TPixel.FromAssociatedScaledVector4(normalized[sample]); |
||||
|
Assert.Equal(expected[i], TPixel.FromAssociatedScaledVector4(source[i])); |
||||
|
} |
||||
|
|
||||
|
PixelOperations<TPixel>.Instance.FromVector4Destructive(Configuration.Default, source, actual, PixelConversionModifiers.Scale | PixelConversionModifiers.Premultiply); |
||||
|
Assert.Equal(expected, actual); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Checks native storage and every scaled output lane independently of integer conversion semantics.
|
||||
|
/// </summary>
|
||||
|
private static void AssertNativeSpecialValuesHaveNormalizedScaledOutput() |
||||
|
{ |
||||
|
Vector4 native = new(float.PositiveInfinity, float.NegativeInfinity, float.NaN, 65504F); |
||||
|
HalfVector4 pixel = HalfVector4.FromVector4(native); |
||||
|
Assert.True(float.IsPositiveInfinity(pixel.ToVector4().X)); |
||||
|
Assert.True(float.IsNegativeInfinity(pixel.ToVector4().Y)); |
||||
|
Assert.True(float.IsNaN(pixel.ToVector4().Z)); |
||||
|
|
||||
|
Vector4 expected = new(1F, 0F, 0F, 1F); |
||||
|
Assert.Equal(expected, pixel.ToScaledVector4()); |
||||
|
Assert.Equal(1F, new HalfSingle(float.PositiveInfinity).ToScaledVector4().X); |
||||
|
Assert.Equal(0F, new HalfSingle(float.NaN).ToScaledVector4().X); |
||||
|
Assert.Equal(new Vector4(1F, 0F, 0F, 1F), new HalfVector2(new Vector2(float.PositiveInfinity, float.NaN)).ToScaledVector4()); |
||||
|
|
||||
|
HalfVector4[] source = new HalfVector4[17]; |
||||
|
Vector4[] nativeSource = new Vector4[source.Length]; |
||||
|
Array.Fill(nativeSource, native); |
||||
|
PixelOperations<HalfVector4>.Instance.FromVector4Destructive(Configuration.Default, nativeSource, source, PixelConversionModifiers.None); |
||||
|
Assert.All(source, value => Assert.Equal(pixel.PackedValue, value.PackedValue)); |
||||
|
|
||||
|
Vector4[] actual = new Vector4[source.Length]; |
||||
|
PixelOperations<HalfVector4>.Instance.ToVector4(Configuration.Default, source, actual, PixelConversionModifiers.Scale); |
||||
|
Assert.All(actual, value => Assert.Equal(expected, value)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
version https://git-lfs.github.com/spec/v1 |
||||
|
oid sha256:1183a3462f92784a0608fef2da95bef92d7f13f6275d4c628cc2310c772085cb |
||||
|
size 38937 |
||||
Loading…
Reference in new issue