From b03f0ed7e5be760eecc740fa53dd166b18ae9b23 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Sep 2026 01:32:03 +1000 Subject: [PATCH] Reject unsupported ICC conversion channels --- .../Icc/Calculators/ClutCalculator.cs | 4 + .../Icc/Calculators/LutEntryCalculator.cs | 5 + .../Icc/Calculators/TrcCalculator.cs | 4 + .../Formats/Png/PngDecoderTests.Icc.cs | 124 ++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs index 82d475e578..b33d7dd449 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs @@ -42,6 +42,10 @@ internal class ClutCalculator : IVector4Calculator Guard.NotNull(clut, nameof(clut)); Guard.MustBeGreaterThan(clut.InputChannelCount, 0, nameof(clut.InputChannelCount)); Guard.MustBeGreaterThan(clut.OutputChannelCount, 0, nameof(clut.OutputChannelCount)); + if (clut.InputChannelCount > 4 || clut.OutputChannelCount > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels."); + } this.inputCount = clut.InputChannelCount; this.outputCount = clut.OutputChannelCount; diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs index c97578ee3f..08f957bbe5 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutEntryCalculator.cs @@ -59,6 +59,11 @@ internal class LutEntryCalculator : IVector4Calculator private void Init(IccLut[] inputCurve, IccLut[] outputCurve, IccClut clut, Matrix4x4 matrix) { + if (inputCurve.Length > 4 || outputCurve.Length > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four input and output channels."); + } + this.inputCurve = InitLut(inputCurve); this.outputCurve = InitLut(outputCurve); this.clutCalculator = new ClutCalculator(clut); diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs index d2fc5d9b55..029be68c51 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/TrcCalculator.cs @@ -15,6 +15,10 @@ internal class TrcCalculator : IVector4Calculator public TrcCalculator(IccTagDataEntry[] entries, bool inverted) { Guard.NotNull(entries, nameof(entries)); + if (entries.Length > 4) + { + throw new InvalidIccProfileException("ICC conversion supports at most four tone response curves."); + } this.calculators = new ISingleCalculator[entries.Length]; for (int i = 0; i < entries.Length; i++) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs new file mode 100644 index 0000000000..25632212c6 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Icc.cs @@ -0,0 +1,124 @@ +// 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(() => Image.Load(options, pngData)); + } + + 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 buffer = stackalloc byte[4]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, value); + stream.Write(buffer); + } + + void WriteUInt16(ushort value) + { + Span 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 image = new(16, 16); + image.Metadata.IccProfile = new IccProfile(profileData); + + using MemoryStream stream = new(); + image.SaveAsPng(stream); + return stream.ToArray(); + } +}