From 0582e85e4957ed29f57d721d4037aa8a7b3076f3 Mon Sep 17 00:00:00 2001 From: Socolin Date: Wed, 10 Dec 2025 23:12:33 -0500 Subject: [PATCH 1/3] Fix decoding tiff image with BigEndian + 64 bit / pixel + associated alpha --- .../Formats/Tiff/Utils/TiffUtilities.cs | 7 ++- .../Formats/Tiff/TiffDecoderTests.cs | 13 +++++ .../Formats/Tiff/Utils/TiffUtilitiesTest.cs | 51 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + ...t_WithAssociatedAlpha_Rgba64_Issue3031.png | 3 ++ ...ha_Rgba64_RgbaAssociatedAlpha16bit_lsb.png | 3 ++ ...ha_Rgba64_RgbaAssociatedAlpha16bit_msb.png | 3 ++ ...tedAlpha_Rgba16161616_Rgba64_Issue3031.png | 3 ++ tests/Images/Input/Tiff/Issues/Issue3031.tiff | 3 ++ 9 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Formats/Tiff/Utils/TiffUtilitiesTest.cs create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_Issue3031.png create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_lsb.png create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_64Bit_WithAssociatedAlpha_Rgba64_RgbaAssociatedAlpha16bit_msb.png create mode 100644 tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_BigEndian_AssociatedAlpha_Rgba16161616_Rgba64_Issue3031.png create mode 100644 tests/Images/Input/Tiff/Issues/Issue3031.tiff 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 @@ public static TPixel ColorFromRgba64Premultiplied(ushort r, ushort g, us 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/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 void TiffDecoder_CanDecode_YccK(TestImageProvider provide 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bc699da88e..161b1a709d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1142,6 +1142,7 @@ public static class Tiff 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/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/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 From 1e48674d380fdcb33ead3fe8b8acfdf5ae82eb8d Mon Sep 17 00:00:00 2001 From: Socolin Date: Mon, 15 Dec 2025 01:33:23 -0500 Subject: [PATCH 2/3] Optimization in Rgba16161616TiffColor, split a code into 2 loops to avoid a condtion for each pixel --- .../Rgba16161616TiffColor{TPixel}.cs | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) 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 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in 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), From c66833c17622c761fb00a1ebcca6c404a4ed9f46 Mon Sep 17 00:00:00 2001 From: Socolin Date: Tue, 16 Dec 2025 02:10:03 -0500 Subject: [PATCH 3/3] Remove retired macos image from test matrix in CI --- .github/workflows/build-and-test.yml | 11 ----------- 1 file changed, 11 deletions(-) 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