diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 99ca63fc39..637373e804 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -69,12 +69,6 @@ jobs:
sdk-preview: true
runtime: -x64
codecov: false
- - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
- framework: net10.0
- sdk: 10.0.x
- sdk-preview: true
- runtime: -x64
- codecov: false
- os: macos-26
framework: net10.0
sdk: 10.0.x
@@ -99,11 +93,6 @@ jobs:
sdk: 8.0.x
runtime: -x64
codecov: false
- - os: macos-13 # macos-latest runs on arm64 runners where libgdiplus is unavailable
- framework: net8.0
- sdk: 8.0.x
- runtime: -x64
- codecov: false
- os: macos-26
framework: net8.0
sdk: 8.0.x
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
index 3ddbf93b58..fd99fb4467 100644
--- a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsIcc.cs
@@ -39,6 +39,24 @@ internal static class ColorProfileConverterExtensionsIcc
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F,
0.0033717495F, 0.0034852044F, 0.0028800198F, 0F];
+ ///
+ /// Converts a color value from one ICC color profile to another using the specified color profile converter.
+ ///
+ ///
+ /// This method performs color conversion using ICC profiles, ensuring accurate color mapping
+ /// between different color spaces. Both the source and target ICC profiles must be provided in the converter's
+ /// options. The method supports perceptual adjustments when required by the profiles.
+ ///
+ /// The type representing the source color profile. Must implement .
+ /// The type representing the destination color profile. Must implement .
+ /// The color profile converter configured with source and target ICC profiles.
+ /// The color value to convert, defined in the source color profile.
+ ///
+ /// A color value in the target color profile, resulting from the ICC profile-based conversion of the source value.
+ ///
+ ///
+ /// Thrown if either the source or target ICC profile is missing from the converter options.
+ ///
internal static TTo ConvertUsingIccProfile(this ColorProfileConverter converter, in TFrom source)
where TFrom : struct, IColorProfile
where TTo : struct, IColorProfile
@@ -81,6 +99,29 @@ internal static class ColorProfileConverterExtensionsIcc
return TTo.FromScaledVector4(targetParams.Converter.Calculate(targetPcs));
}
+ ///
+ /// Converts a span of color values from a source color profile to a destination color profile using ICC profiles.
+ ///
+ ///
+ /// This method performs color conversion by transforming the input values through the Profile
+ /// Connection Space (PCS) as defined by the provided ICC profiles. Perceptual adjustments are applied as required
+ /// by the profiles. The method does not support absolute colorimetric intent and will not perform such
+ /// conversions.
+ ///
+ /// The type representing the source color profile. Must implement .
+ /// The type representing the destination color profile. Must implement .
+ /// The color profile converter that provides conversion options and ICC profiles.
+ ///
+ /// A read-only span containing the source color values to convert. The values must conform to the source color
+ /// profile.
+ ///
+ ///
+ /// A span to receive the converted color values in the destination color profile. Must be at least as large as the
+ /// source span.
+ ///
+ ///
+ /// Thrown if the source or target ICC profile is missing from the converter options.
+ ///
internal static void ConvertUsingIccProfile(this ColorProfileConverter converter, ReadOnlySpan source, Span destination)
where TFrom : struct, IColorProfile
where TTo : struct, IColorProfile
diff --git a/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
new file mode 100644
index 0000000000..2780f04bae
--- /dev/null
+++ b/src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Buffers;
+using System.Numerics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+
+namespace SixLabors.ImageSharp.ColorProfiles;
+
+internal static class ColorProfileConverterExtensionsPixelCompatible
+{
+ ///
+ /// Converts the pixel data of the specified image from the source color profile to the target color profile using
+ /// the provided color profile converter.
+ ///
+ ///
+ /// This method modifies the source image in place by converting its pixel data according to the
+ /// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible;
+ /// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC
+ /// profiles are set on the converter before calling this method.
+ ///
+ /// The pixel format.
+ /// The color profile converter configured with source and target ICC profiles.
+ ///
+ /// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
+ /// image.
+ ///
+ ///
+ /// Thrown if the converter's source or target ICC profile is not specified.
+ ///
+ public static void Convert(this ColorProfileConverter converter, Image source)
+ where TPixel : unmanaged, IPixel
+ {
+ // These checks actually take place within the converter, but we want to fail fast here.
+ // Note. we do not check to see whether the profiles themselves are RGB compatible,
+ // if they are not, then the converter will simply produce incorrect results.
+ if (converter.Options.SourceIccProfile is null)
+ {
+ throw new InvalidOperationException("Source ICC profile is missing.");
+ }
+
+ if (converter.Options.TargetIccProfile is null)
+ {
+ throw new InvalidOperationException("Target ICC profile is missing.");
+ }
+
+ // Process the rows in parallel chunks, the converter itself is thread safe.
+ source.Mutate(o => o.ProcessPixelRowsAsVector4(
+ row =>
+ {
+ // Gather and convert the pixels in the row to Rgb.
+ using IMemoryOwner rgbBuffer = converter.Options.MemoryAllocator.Allocate(row.Length);
+ Span rgbSpan = rgbBuffer.Memory.Span;
+ Rgb.FromScaledVector4(row, rgbSpan);
+
+ // Perform the actual color conversion.
+ converter.ConvertUsingIccProfile(rgbSpan, rgbSpan);
+
+ // Copy the converted Rgb pixels back to the row as TPixel.
+ ref Vector4 rowRef = ref MemoryMarshal.GetReference(row);
+ for (int i = 0; i < rgbSpan.Length; i++)
+ {
+ Vector3 rgb = rgbSpan[i].AsVector3Unsafe();
+ Unsafe.As(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb;
+ }
+ },
+ PixelConversionModifiers.Scale));
+ }
+}
diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs
index adf0107da0..da50a1abec 100644
--- a/src/ImageSharp/Formats/ImageDecoderCore.cs
+++ b/src/ImageSharp/Formats/ImageDecoderCore.cs
@@ -1,8 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.ColorProfiles;
+using SixLabors.ImageSharp.ColorProfiles.Icc;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
+using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats;
@@ -124,4 +127,41 @@ internal abstract class ImageDecoderCore
///
protected abstract Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel;
+
+ ///
+ /// Converts the ICC color profile of the specified image to the compact sRGB v4 profile if a source profile is
+ /// available.
+ ///
+ ///
+ /// This method should only be used by decoders that gurantee that the encoded image data is in a color space
+ /// compatible with sRGB (e.g. standard RGB, Adobe RGB, ProPhoto RGB).
+ ///
+ /// If the image does not have a valid ICC profile for color conversion, no changes are made.
+ /// This operation may affect the color appearance of the image to ensure consistency with the sRGB color
+ /// space.
+ ///
+ /// The pixel format.
+ /// The image whose ICC profile will be converted to the compact sRGB v4 profile.
+ ///
+ /// if the conversion was performed; otherwise, .
+ ///
+ protected bool TryConvertIccProfile(Image image)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!this.Options.TryGetIccProfileForColorConversion(image.Metadata.IccProfile, out IccProfile? profile))
+ {
+ return false;
+ }
+
+ ColorConversionOptions options = new()
+ {
+ SourceIccProfile = profile,
+ TargetIccProfile = CompactSrgbV4Profile.Profile,
+ MemoryAllocator = image.Configuration.MemoryAllocator,
+ };
+
+ ColorProfileConverter converter = new(options);
+ converter.Convert(image);
+ return true;
+ }
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 38f964d37b..bff4d30ee5 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -212,6 +212,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan());
break;
case PngChunkType.FrameData:
+ {
if (frameCount >= this.maxFrames)
{
goto EOF;
@@ -246,7 +247,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
+ }
+
case PngChunkType.Data:
+ {
pngMetadata.AnimateRootFrame = currentFrameControl != null;
currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height);
if (image is null)
@@ -276,6 +280,8 @@ internal sealed class PngDecoderCore : ImageDecoderCore
}
break;
+ }
+
case PngChunkType.Palette:
this.palette = chunk.Data.GetSpan().ToArray();
break;
@@ -323,6 +329,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
PngThrowHelper.ThrowNoData();
}
+ _ = this.TryConvertIccProfile(image);
return image;
}
catch
diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs
index cecdf88c9d..1cf4bb6ed6 100644
--- a/src/ImageSharp/Formats/Png/PngMetadata.cs
+++ b/src/ImageSharp/Formats/Png/PngMetadata.cs
@@ -111,7 +111,7 @@ public class PngMetadata : IFormatMetadata
color = PngColorType.Rgb;
break;
default:
- if (colorType.HasFlag(PixelColorType.Luminance))
+ if (colorType.HasFlag(PixelColorType.Luminance | PixelColorType.Alpha))
{
color = PngColorType.GrayscaleWithAlpha;
break;
diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs
index 9847f45b54..086aa3b4e1 100644
--- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs
+++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgba16161616TiffColor{TPixel}.cs
@@ -1,5 +1,6 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+
#nullable disable
using System.Buffers;
@@ -48,31 +49,53 @@ internal class Rgba16161616TiffColor : TiffBaseColorDecoder
using IMemoryOwner vectors = hasAssociatedAlpha ? this.memoryAllocator.Allocate(width) : null;
Span vectorsSpan = hasAssociatedAlpha ? vectors.GetSpan() : [];
- for (int y = top; y < top + height; y++)
- {
- Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
- if (this.isBigEndian)
+ if (this.isBigEndian)
+ {
+ if (hasAssociatedAlpha)
{
- for (int x = 0; x < pixelRow.Length; x++)
+ for (int y = top; y < top + height; y++)
{
- ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
- offset += 2;
- ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
- offset += 2;
- ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
- offset += 2;
- ushort a = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
- offset += 2;
-
- pixelRow[x] = hasAssociatedAlpha
- ? TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a)
- : TPixel.FromRgba64(new Rgba64(r, g, b, a));
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+
+ for (int x = 0; x < pixelRow.Length; x++)
+ {
+ ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
+ ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 2, 2));
+ ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 4, 2));
+ ushort a = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 6, 2));
+ offset += 8;
+
+ pixelRow[x] = TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a);
+ }
}
}
else
{
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
+
+ for (int x = 0; x < pixelRow.Length; x++)
+ {
+ ushort r = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset, 2));
+ ushort g = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 2, 2));
+ ushort b = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 4, 2));
+ ushort a = TiffUtilities.ConvertToUShortBigEndian(data.Slice(offset + 6, 2));
+ offset += 8;
+
+ pixelRow[x] = TPixel.FromRgba64(new Rgba64(r, g, b, a));
+ }
+ }
+ }
+ }
+ else
+ {
+ for (int y = top; y < top + height; y++)
+ {
+ Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width);
int byteCount = pixelRow.Length * 8;
+
PixelOperations.Instance.FromRgba64Bytes(
this.configuration,
data.Slice(offset, byteCount),
diff --git a/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs b/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs
index e30765b1f4..b7d412f3c8 100644
--- a/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs
+++ b/src/ImageSharp/Formats/Tiff/Utils/TiffUtilities.cs
@@ -45,7 +45,12 @@ internal static class TiffUtilities
return TPixel.FromRgba64(default);
}
- return TPixel.FromRgba64(new Rgba64((ushort)(r / a), (ushort)(g / a), (ushort)(b / a), a));
+ float scale = 65535f / a;
+ ushort ur = (ushort)Math.Min(r * scale, 65535);
+ ushort ug = (ushort)Math.Min(g * scale, 65535);
+ ushort ub = (ushort)Math.Min(b * scale, 65535);
+
+ return TPixel.FromRgba64(new Rgba64(ur, ug, ub, a));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
index 491f716500..7cec7c1db8 100644
--- a/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
+++ b/src/ImageSharp/Formats/Webp/Chunks/WebpVp8X.cs
@@ -123,7 +123,10 @@ internal readonly struct WebpVp8X : IEquatable
long pos = RiffHelper.BeginWriteChunk(stream, (uint)WebpChunkType.Vp8X);
stream.WriteByte(flags);
- stream.Position += 3; // Reserved bytes
+
+ Span reserved = stackalloc byte[3];
+ stream.Write(reserved);
+
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Width - 1);
WebpChunkParsingUtils.WriteUInt24LittleEndian(stream, this.Height - 1);
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
index 6b31cadf4f..632e1bec04 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
@@ -39,13 +39,13 @@ internal struct UnmanagedMemoryHandle : IEquatable
Interlocked.Increment(ref totalOutstandingHandles);
}
- public IntPtr Handle => this.handle;
+ public readonly IntPtr Handle => this.handle;
- public bool IsInvalid => this.Handle == IntPtr.Zero;
+ public readonly bool IsInvalid => this.Handle == IntPtr.Zero;
- public bool IsValid => this.Handle != IntPtr.Zero;
+ public readonly bool IsValid => this.Handle != IntPtr.Zero;
- public unsafe void* Pointer => (void*)this.Handle;
+ public readonly unsafe void* Pointer => (void*)this.Handle;
///
/// Gets the total outstanding handle allocations for testing purposes.
@@ -121,9 +121,9 @@ internal struct UnmanagedMemoryHandle : IEquatable
this.lengthInBytes = 0;
}
- public bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
+ public readonly bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle);
- public override bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
+ public override readonly bool Equals(object? obj) => obj is UnmanagedMemoryHandle other && this.Equals(other);
- public override int GetHashCode() => this.handle.GetHashCode();
+ public override readonly int GetHashCode() => this.handle.GetHashCode();
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 3589a25a2d..a58101a6bd 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -206,6 +206,22 @@ public partial class PngDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact);
}
+ [Theory]
+ [WithFile(TestImages.Png.Icc.Perceptual, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Icc.PerceptualcLUTOnly, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Icc.SRgbGray, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba32, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba64, PixelTypes.Rgba32)]
+ public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert });
+
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(provider);
+ Assert.Null(image.Metadata.IccProfile);
+ }
+
[Theory]
[WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
index 5cbc27611a..a0c552a221 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
@@ -336,4 +336,29 @@ public class PngMetadataTests
Assert.Equal(42, (int)exif.GetValue(ExifTag.ImageNumber).Value);
}
+
+
+ [Theory]
+ [InlineData(PixelColorType.Binary, PngColorType.Palette)]
+ [InlineData(PixelColorType.Indexed, PngColorType.Palette)]
+ [InlineData(PixelColorType.Luminance, PngColorType.Grayscale)]
+ [InlineData(PixelColorType.RGB, PngColorType.Rgb)]
+ [InlineData(PixelColorType.BGR, PngColorType.Rgb)]
+ [InlineData(PixelColorType.YCbCr, PngColorType.RgbWithAlpha)]
+ [InlineData(PixelColorType.CMYK, PngColorType.RgbWithAlpha)]
+ [InlineData(PixelColorType.YCCK, PngColorType.RgbWithAlpha)]
+ public void FromFormatConnectingMetadata_ConvertColorTypeAsExpected(PixelColorType pixelColorType, PngColorType expectedPngColorType)
+ {
+ FormatConnectingMetadata formatConnectingMetadata = new()
+ {
+ PixelTypeInfo = new PixelTypeInfo(24)
+ {
+ ColorType = pixelColorType,
+ },
+ };
+
+ PngMetadata actual = PngMetadata.FromFormatConnectingMetadata(formatConnectingMetadata);
+
+ Assert.Equal(expectedPngColorType, actual.ColorType);
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index d850c67a51..5096d93bd7 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -365,6 +365,19 @@ public class TiffDecoderTests : TiffDecoderBaseTester
image.CompareToReferenceOutput(ImageComparer.TolerantPercentage(0.0001F), provider);
}
+ [Theory]
+ [WithFile(Issues3031, PixelTypes.Rgba64)]
+ [WithFile(Rgba16BitAssociatedAlphaBigEndian, PixelTypes.Rgba64)]
+ [WithFile(Rgba16BitAssociatedAlphaLittleEndian, PixelTypes.Rgba64)]
+ public void TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(TiffDecoder.Instance);
+ image.DebugSave(provider);
+
+ image.CompareToReferenceOutput(ImageComparer.Exact, provider);
+ }
+
[Theory]
[WithFile(Issues2454_A, PixelTypes.Rgba32)]
[WithFile(Issues2454_B, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs
new file mode 100644
index 0000000000..6483b63c52
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Tiff.Utils;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Tiff.Utils;
+
+[Trait("Format", "Tiff")]
+public class TiffUtilitiesTest
+{
+ [Theory]
+ [InlineData(0, 0, 0, 0)]
+ [InlineData(42, 84, 128, 0)]
+ [InlineData(65535, 65535, 65535, 0)]
+ public void ColorFromRgba64Premultiplied_WithZeroAlpha_ReturnsDefaultPixel(ushort r, ushort g, ushort b, ushort a)
+ {
+ Rgba64 actual = TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a);
+
+ Assert.Equal(default, actual);
+ }
+
+ [Theory]
+ [InlineData(65535, 65535, 65535, 65535, 65535, 65535, 65535, 65535)]
+ [InlineData(32767, 0, 0, 65535, 32767, 0, 0, 65535)]
+ [InlineData(0, 32767, 0, 65535, 0, 32767, 0, 65535)]
+ [InlineData(0, 0, 32767, 65535, 0, 0, 32767, 65535)]
+ public void ColorFromRgba64Premultiplied_WithNoAlpha_ReturnExpectedValues(ushort r, ushort g, ushort b, ushort a, ushort expectedR, ushort expectedG, ushort expectedB, ushort expectedA)
+ {
+ Rgba64 actual = TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a);
+
+ Assert.Equal(new Rgba64(expectedR, expectedG, expectedB, expectedA), actual);
+ }
+
+ [Theory]
+ [InlineData(32766, 0, 0, 32766, 65535, 0, 0, 32766)] // Red, 50% Alpha
+ [InlineData(0, 32766, 0, 32766, 0, 65535, 0, 32766)] // Green, 50% Alpha
+ [InlineData(0, 0, 32766, 32766, 0, 0, 65535, 32766)] // Blue, 50% Alpha
+ [InlineData(8191, 0, 0, 16383, 32765, 0, 0, 16383)] // Red, 25% Alpha
+ [InlineData(0, 8191, 0, 16383, 0, 32765, 0, 16383)] // Green, 25% Alpha
+ [InlineData(0, 0, 8191, 16383, 0, 0, 32765, 16383)] // Blue, 25% Alpha
+ [InlineData(8191, 0, 0, 0, 0, 0, 0, 0)] // Red, 0% Alpha
+ [InlineData(0, 8191, 0, 0, 0, 0, 0, 0)] // Green, 0% Alpha
+ [InlineData(0, 0, 8191, 0, 0, 0, 0, 0)] // Blue, 0% Alpha
+ public void ColorFromRgba64Premultiplied_WithAlpha_ReturnExpectedValues(ushort r, ushort g, ushort b, ushort a, ushort expectedR, ushort expectedG, ushort expectedB, ushort expectedA)
+ {
+ Rgba64 actual = TiffUtilities.ColorFromRgba64Premultiplied(r, g, b, a);
+
+ Assert.Equal(new Rgba64(expectedR, expectedG, expectedB, expectedA), actual);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpVp8XTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpVp8XTests.cs
new file mode 100644
index 0000000000..78be0bf9fd
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpVp8XTests.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Webp.Chunks;
+
+namespace SixLabors.ImageSharp.Tests.Formats.WebP;
+
+[Trait("Format", "Webp")]
+public class WebpVp8XTests
+{
+ [Fact]
+ public void WebpVp8X_WriteTo_Writes_Reserved_Bytes()
+ {
+ // arrange
+ WebpVp8X header = new(false, false, false, false, false, 10, 40);
+ MemoryStream ms = new();
+ byte[] expected = [86, 80, 56, 88, 10, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 39, 0, 0];
+
+ // act
+ header.WriteTo(ms);
+
+ // assert
+ byte[] actual = ms.ToArray();
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
index 6e7e9fea73..c1f5b44bf7 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
+++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
+using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor;
@@ -273,67 +274,75 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
[InlineData(1200)] // Group of two UniformUnmanagedMemoryPool buffers
public void AllocateMemoryGroup_Finalization_ReturnsToPool(int length)
{
- if (TestEnvironment.IsMacOS)
- {
- // Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
- return;
- }
-
- if (TestEnvironment.OSArchitecture == Architecture.Arm64)
- {
- // Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
- return;
- }
-
- if (!TestEnvironment.RunsOnCI)
- {
- // This may fail in local runs resulting in high memory load.
- // Remove the condition for local debugging!
- return;
- }
-
- // RunTest(length.ToString());
- RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose();
+ RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose();
static void RunTest(string lengthStr)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024);
- int lengthInner = int.Parse(lengthStr);
-
+ int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture);
+
+ // We want to verify that a leaked (not disposed) `MemoryGroup` still returns its
+ // unmanaged handles into the pool when it is finalized.
+ //
+ // We intentionally do NOT validate this by checking the contents of the re-rented memory
+ // (contents are not guaranteed to be preserved) nor by comparing pointer values
+ // (the pool may return a different handle while still correctly pooling).
+ //
+ // Instead, we validate that after a forced GC+finalization cycle, a subsequent allocation
+ // of the same size does not cause the number of outstanding unmanaged handles to *increase*
+ // compared to a known baseline.
+
+ // Establish a baseline: create one allocation and dispose it so the pool is initialized.
+ // (This ensures subsequent observations are not biased by first-time pool growth.)
+ allocator.AllocateGroup(lengthInner, 100).Dispose();
+ int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles;
+
+ // Leak one allocation and force finalization.
AllocateGroupAndForget(allocator, lengthInner);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
- AllocateGroupAndForget(allocator, lengthInner, true);
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- GC.WaitForPendingFinalizers();
-
- using MemoryGroup g = allocator.AllocateGroup(lengthInner, 100);
- Assert.Equal(42, g.First().Span[0]);
+ // Allocate again. If the leaked group was finalized correctly and returned to the pool,
+ // this should not require additional unmanaged allocations (ie, the handle count must not grow).
+ allocator.AllocateGroup(lengthInner, 100).Dispose();
+
+ // Note: we use "<=" instead of "==" here.
+ //
+ // After we record the baseline, the pool is allowed to legitimately *decrease*
+ // `UnmanagedMemoryHandle.TotalOutstandingHandles` by trimming retained buffers
+ // (eg. via the pool's trim timer/GC callbacks/high-pressure logic).
+ //
+ // What must not happen is the opposite: the leaked (non-disposed) group should be finalized
+ // and its handles returned to the pool such that allocating again does NOT require creating
+ // additional unmanaged handles. Therefore the only invariant we can reliably assert here is
+ // "no growth" relative to the baseline.
+ Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles);
}
}
- private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
{
+ // Allocate a group and drop the reference without disposing.
+ // The test relies on the group's finalizer to return the rented memory to the pool.
MemoryGroup g = allocator.AllocateGroup(length, 100);
- if (check)
- {
- Assert.Equal(42, g.First().Span[0]);
- }
- g.First().Span[0] = 42;
+ // Touch the memory to ensure the buffer is actually materialized/usable.
+ g[0].Span[0] = 42;
if (length < 512)
{
- // For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
- // repeat rental to make sure per-core buckets are also utilized.
+ // For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
+ // Repeat rental to increase the chance that per-core buckets are involved when length
+ // is small and allocations go through ArrayPool.
MemoryGroup g1 = allocator.AllocateGroup(length, 100);
- g1.First().Span[0] = 42;
+ g1[0].Span[0] = 42;
+ g1 = null;
}
+
+ g = null;
}
[Theory]
@@ -341,69 +350,63 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
[InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer
public void AllocateSingleMemoryOwner_Finalization_ReturnsToPool(int length)
{
- if (TestEnvironment.IsMacOS)
- {
- // Skip on macOS: https://github.com/SixLabors/ImageSharp/issues/1887
- return;
- }
-
- if (TestEnvironment.OSArchitecture == Architecture.Arm64)
- {
- // Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
- return;
- }
-
- if (!TestEnvironment.RunsOnCI)
- {
- // This may fail in local runs resulting in high memory load.
- // Remove the condition for local debugging!
- return;
- }
-
- // RunTest(length.ToString());
- RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose();
+ RemoteExecutor.Invoke(RunTest, length.ToString(CultureInfo.InvariantCulture)).Dispose();
static void RunTest(string lengthStr)
{
UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(512, 1024, 16 * 1024, 1024);
- int lengthInner = int.Parse(lengthStr);
-
+ int lengthInner = int.Parse(lengthStr, CultureInfo.InvariantCulture);
+
+ // This test verifies pooling behavior when an `IMemoryOwner` is leaked (not disposed)
+ // and must be returned to the pool by finalization.
+ //
+ // We do NOT use a sentinel byte value to prove reuse because the contents of pooled buffers
+ // are not required to be preserved across rentals.
+ //
+ // Instead, we assert that after forcing GC+finalization, renting the same size again does not
+ // increase `UnmanagedMemoryHandle.TotalOutstandingHandles` above a baseline.
+
+ // Establish a baseline: allocate+dispose once so the pool has a chance to materialize/retain buffers.
+ allocator.Allocate(lengthInner).Dispose();
+ int baselineHandles = UnmanagedMemoryHandle.TotalOutstandingHandles;
+
+ // Leak one allocation and force finalization.
AllocateSingleAndForget(allocator, lengthInner);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
- AllocateSingleAndForget(allocator, lengthInner, true);
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- GC.WaitForPendingFinalizers();
+ // Allocate again. If the leaked owner was finalized correctly and returned to the pool,
+ // this should not require additional unmanaged allocations (ie, the handle count must not grow).
+ allocator.Allocate(lengthInner).Dispose();
- using IMemoryOwner g = allocator.Allocate(lengthInner);
- Assert.Equal(42, g.GetSpan()[0]);
- GC.KeepAlive(allocator);
+ // Note: we use "<=" rather than "==". The pool may legitimately trim and free retained buffers,
+ // reducing the handle count between baseline and check. The invariant is "no growth".
+ Assert.True(UnmanagedMemoryHandle.TotalOutstandingHandles <= baselineHandles);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
- private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false)
+ private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length)
{
+ // Allocate and intentionally do not dispose.
IMemoryOwner g = allocator.Allocate(length);
- if (check)
- {
- Assert.Equal(42, g.GetSpan()[0]);
- }
+ // Touch the memory to ensure the buffer is actually materialized/usable.
g.GetSpan()[0] = 42;
if (length < 512)
{
- // For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread,
- // repeat rental to make sure per-core buckets are also utilized.
+ // For ArrayPool.Shared, the first rented array may be stored in TLS on the finalizer thread.
+ // Repeat rental to increase the chance that per-core buckets are involved when length
+ // is small and allocations go through ArrayPool.
IMemoryOwner g1 = allocator.Allocate(length);
g1.GetSpan()[0] = 42;
+ g1 = null;
}
+
+ g = null;
}
[Fact]
diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
index 395dfd455f..6c8dd2618c 100644
--- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
+++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs
@@ -20,20 +20,13 @@ public static class MemoryAllocatorValidator
private static void MemoryDiagnostics_MemoryReleased()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
- if (backing != null)
- {
- backing.TotalRemainingAllocated--;
- }
+ backing?.OnReleased();
}
private static void MemoryDiagnostics_MemoryAllocated()
{
TestMemoryDiagnostics backing = LocalInstance.Value;
- if (backing != null)
- {
- backing.TotalAllocated++;
- backing.TotalRemainingAllocated++;
- }
+ backing?.OnAllocated();
}
public static TestMemoryDiagnostics MonitorAllocations()
@@ -48,11 +41,23 @@ public static class MemoryAllocatorValidator
public static void ValidateAllocations(int expectedAllocationCount = 0)
=> LocalInstance.Value?.Validate(expectedAllocationCount);
- public class TestMemoryDiagnostics : IDisposable
+ public sealed class TestMemoryDiagnostics : IDisposable
{
- public int TotalAllocated { get; set; }
+ private int totalAllocated;
+ private int totalRemainingAllocated;
+
+ public int TotalAllocated => Volatile.Read(ref this.totalAllocated);
+
+ public int TotalRemainingAllocated => Volatile.Read(ref this.totalRemainingAllocated);
+
+ internal void OnAllocated()
+ {
+ Interlocked.Increment(ref this.totalAllocated);
+ Interlocked.Increment(ref this.totalRemainingAllocated);
+ }
- public int TotalRemainingAllocated { get; set; }
+ internal void OnReleased()
+ => Interlocked.Decrement(ref this.totalRemainingAllocated);
public void Validate(int expectedAllocationCount)
{
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index bc699da88e..f6cd776e47 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -166,6 +166,15 @@ public static class TestImages
// Issue 3000: https://github.com/SixLabors/ImageSharp/issues/3000
public const string Issue3000 = "Png/issues/issue_3000.png";
+ public static class Icc
+ {
+ public const string SRgbGray = "Png/icc-profiles/sRGB_Gray.png";
+ public const string SRgbGrayInterlacedRgba32 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png";
+ public const string SRgbGrayInterlacedRgba64 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png";
+ public const string Perceptual = "Png/icc-profiles/Perceptual.png";
+ public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png";
+ }
+
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
@@ -1142,6 +1151,7 @@ public static class TestImages
public const string Issues2435 = "Tiff/Issues/Issue2435.tiff";
public const string Issues2454_A = "Tiff/Issues/Issue2454_A.tif";
public const string Issues2454_B = "Tiff/Issues/Issue2454_B.tif";
+ public const string Issues3031 = "Tiff/Issues/Issue3031.tiff";
public const string Issues2587 = "Tiff/Issues/Issue2587.tiff";
public const string Issues2679 = "Tiff/Issues/Issue2679.tiff";
public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff";
diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png
new file mode 100644
index 0000000000..ffc9839019
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b72c885278a066e63c013885c42b772275f25a5f0b2290aa38c87f3dbeac984b
+size 81432
diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png
new file mode 100644
index 0000000000..25a97ca48d
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:936261278b1a9f5bf9a2bb4f8da09f2a82e1b5c693790e137c5f98fa4d885735
+size 81785
diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png
new file mode 100644
index 0000000000..5a35cf5796
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:bf856e49e4ece7e59eea684f6fa533ba313a36955be4703894f16b100283cb4a
+size 2687
diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png
new file mode 100644
index 0000000000..270555a555
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:337e84b78fb07359a42e7eee0eed32e6728497c64aa30c6bd5ea8a3a5ec67ebc
+size 5151
diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png
new file mode 100644
index 0000000000..dc5f4a559c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:456ae30184b13aa2dc3d922db433017e076ff969862fe506436ed96c2d9be0a1
+size 6143
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_Issue3031.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_Issue3031.png
new file mode 100644
index 0000000000..d5a017475f
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_Issue3031.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c33a2f63836975bdc7631f26634b3fb0ae98bfaff730300877339cb568141cde
+size 124
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_lsb.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_lsb.png
new file mode 100644
index 0000000000..a3317a6b48
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_lsb.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e50dfa103459d21642df5e1ca760081fbdfe3b7244624d9d87d8a20f45b51bbe
+size 117953
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png
new file mode 100644
index 0000000000..5b72c3732c
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3333503012aa29e23f0d0e52993ad3499514251208fb3318e2bb1560d54650fa
+size 117956
diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_BigEndian_AssociatedAlpha_Rgba16161616_Rgba64_Issue3031.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_BigEndian_AssociatedAlpha_Rgba16161616_Rgba64_Issue3031.png
new file mode 100644
index 0000000000..d5a017475f
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_BigEndian_AssociatedAlpha_Rgba16161616_Rgba64_Issue3031.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c33a2f63836975bdc7631f26634b3fb0ae98bfaff730300877339cb568141cde
+size 124
diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png
new file mode 100644
index 0000000000..8ac1afde9f
--- /dev/null
+++ b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0c734cacc2c6e761bab088cac80ef09da7b56a545ce71c6cced4cac31e661795
+size 119811
diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual.png b/tests/Images/Input/Png/icc-profiles/Perceptual.png
new file mode 100644
index 0000000000..cac9bcb1e5
--- /dev/null
+++ b/tests/Images/Input/Png/icc-profiles/Perceptual.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:208a325dedea4453b7accce1ec540452af2e9be0f8c1f636f1d61a463eb3a9ae
+size 123151
diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png
new file mode 100644
index 0000000000..3326936ceb
--- /dev/null
+++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c64e0f6cc38750c83e6ff0cf1911e210c342900bb2cd6c88d3daed30c854e863
+size 4531
diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png
new file mode 100644
index 0000000000..7afc51cfe3
--- /dev/null
+++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4fc63cea5de188e76503bde2fce3ff84518af5064bb46d506420cd6d7e58285b
+size 7237
diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png
new file mode 100644
index 0000000000..822aca4f53
--- /dev/null
+++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:64343871be4ad61451ef968fa9f07c6a11dee65d0f8fd718ae8c4941586aa60c
+size 8227
diff --git a/tests/Images/Input/Tiff/Issues/Issue3031.tiff b/tests/Images/Input/Tiff/Issues/Issue3031.tiff
new file mode 100644
index 0000000000..bc3ef7d7cf
--- /dev/null
+++ b/tests/Images/Input/Tiff/Issues/Issue3031.tiff
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6e4d2db56a1b7fdea09ed65eab1d10a02952821f6662ca77caa44b8c51b30310
+size 416