From 34e7f9700d8e68bf00c27cffa678b0dfcf539365 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 14:44:03 +1000 Subject: [PATCH 1/8] Fix #3067 --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 19 ++++++++++----- .../Formats/Bmp/BmpDecoderTests.cs | 23 +++++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 17c1f545b7..db53c69464 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -130,6 +130,13 @@ internal sealed class BmpDecoderCore : ImageDecoderCore Image? image = null; try { + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; + + if (bitsPerPixel is not (1 or 2 or 4 or 8 or 16 or 24 or 32)) + { + BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}"); + } + int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); image = new Image(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); @@ -138,23 +145,23 @@ internal sealed class BmpDecoderCore : ImageDecoderCore switch (this.infoHeader.Compression) { - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: + case BmpCompression.RGB when bitsPerPixel is 32 && this.bmpMetadata.InfoHeaderType is BmpInfoHeaderType.WinVersion3: this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 32: + case BmpCompression.RGB when bitsPerPixel is 32: this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 24: + case BmpCompression.RGB when bitsPerPixel is 24: this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is 16: + case BmpCompression.RGB when bitsPerPixel is 16: this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8 && this.processedAlphaMask: + case BmpCompression.RGB when bitsPerPixel is <= 8 && this.processedAlphaMask: this.ReadRgbPaletteWithAlphaMask( stream, pixels, @@ -166,7 +173,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore inverted); break; - case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8: + case BmpCompression.RGB when bitsPerPixel is <= 8: this.ReadRgbPalette( stream, pixels, diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 94cfe85ee5..9487c4ce9a 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -571,4 +571,27 @@ public class BmpDecoderTests }); Assert.IsType(ex.InnerException); } + + [Fact] + public void BmpDecoder_ThrowsException_Issue3067() + { + // Construct minimal BMP with bitsPerPixel = 0 + byte[] bmp = new byte[54]; + bmp[0] = (byte)'B'; + bmp[1] = (byte)'M'; + BitConverter.GetBytes(54).CopyTo(bmp, 2); + BitConverter.GetBytes(54).CopyTo(bmp, 10); + BitConverter.GetBytes(40).CopyTo(bmp, 14); + BitConverter.GetBytes(1).CopyTo(bmp, 18); + BitConverter.GetBytes(1).CopyTo(bmp, 22); + BitConverter.GetBytes((short)1).CopyTo(bmp, 26); + BitConverter.GetBytes((short)0).CopyTo(bmp, 28); // bitsPerPixel = 0 + + using MemoryStream stream = new(bmp); + + InvalidImageContentException ex = Assert.Throws(() => + { + using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream); + }); + } } From 07d2c04bf19b4698b6aca9b9bb4d4a33ba0f7622 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 14:51:01 +1000 Subject: [PATCH 2/8] Potential fix for pull request finding 'Useless assignment to local variable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 9487c4ce9a..6ebe1bf4e0 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -589,7 +589,7 @@ public class BmpDecoderTests using MemoryStream stream = new(bmp); - InvalidImageContentException ex = Assert.Throws(() => + Assert.Throws(() => { using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream); }); From 1d87e61a96640140d45eb8a1c3deb9af654d4363 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 15:05:47 +1000 Subject: [PATCH 3/8] Update BmpDecoderCore.cs --- src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index db53c69464..a1de790c3d 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -130,14 +130,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore Image? image = null; try { - ushort bitsPerPixel = this.infoHeader.BitsPerPixel; - - if (bitsPerPixel is not (1 or 2 or 4 or 8 or 16 or 24 or 32)) - { - BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}"); - } - int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); + ushort bitsPerPixel = this.infoHeader.BitsPerPixel; image = new Image(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); @@ -149,19 +143,23 @@ internal sealed class BmpDecoderCore : ImageDecoderCore this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; + case BmpCompression.RGB when bitsPerPixel is 32: this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; + case BmpCompression.RGB when bitsPerPixel is 24: this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; + case BmpCompression.RGB when bitsPerPixel is 16: this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); break; - case BmpCompression.RGB when bitsPerPixel is <= 8 && this.processedAlphaMask: + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8 && this.processedAlphaMask: this.ReadRgbPaletteWithAlphaMask( stream, pixels, @@ -173,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore inverted); break; - case BmpCompression.RGB when bitsPerPixel is <= 8: + + case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8: this.ReadRgbPalette( stream, pixels, @@ -186,6 +185,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore break; + case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32: + BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}"); + break; + case BmpCompression.RLE24: this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); From 128d6f943a8e6b190352db53674e3ef87fb15d36 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 15:31:20 +1000 Subject: [PATCH 4/8] Update IcoDecoderTests.cs --- .../Formats/Icon/Ico/IcoDecoderTests.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs index 85ff51b185..539826799e 100644 --- a/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs @@ -204,12 +204,19 @@ public class IcoDecoderTests } [Theory] - [WithFile(InvalidAll, PixelTypes.Rgba32)] [WithFile(InvalidBpp, PixelTypes.Rgba32)] + public void InvalidThrows_InvalidImageContentException(TestImageProvider provider) + => Assert.Throws(() => + { + using Image image = provider.GetImage(IcoDecoder.Instance); + }); + + [Theory] + [WithFile(InvalidAll, PixelTypes.Rgba32)] [WithFile(InvalidCompression, PixelTypes.Rgba32)] [WithFile(InvalidRLE4, PixelTypes.Rgba32)] [WithFile(InvalidRLE8, PixelTypes.Rgba32)] - public void InvalidTest(TestImageProvider provider) + public void InvalidThows_NotSupportedException(TestImageProvider provider) => Assert.Throws(() => { using Image image = provider.GetImage(IcoDecoder.Instance); From ae9ca4c5bf56f1dec79c9fe96461fbfd5d5434b2 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 22:30:35 +1000 Subject: [PATCH 5/8] Fix #3064 R --- .../LutABCalculator.CalculationType.cs | 17 ++- .../Icc/Calculators/LutABCalculator.cs | 141 ++++++++++-------- .../Icc/IccConverterbase.Conversions.cs | 2 +- .../DataReader/IccDataReader.TagDataEntry.cs | 110 +++++--------- .../Metadata/Profiles/ICC/IccProfile.cs | 6 +- .../TagDataEntries/IccLutAToBTagDataEntry.cs | 121 ++++++++------- .../TagDataEntries/IccLutBToATagDataEntry.cs | 111 ++++++++------ .../Formats/Jpg/JpegDecoderTests.cs | 15 ++ tests/ImageSharp.Tests/TestImages.cs | 1 + ...B_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png | 3 + .../Input/Jpg/icc-profiles/issue-3064.jpg | 3 + 11 files changed, 288 insertions(+), 242 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png create mode 100644 tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs index 253239cb79..60a7e50b91 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs @@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc; internal partial class LutABCalculator { + /// + /// Identifies the transform direction for the configured LUT calculator. + /// private enum CalculationType { - AtoB = 1 << 3, - BtoA = 1 << 4, + /// + /// Converts from device space to PCS using ICC mAB stage order. + /// + AtoB, - SingleCurve = 1, - CurveMatrix = 2, - CurveClut = 3, - Full = 4, + /// + /// Converts from PCS to device space using ICC mBA stage order. + /// + BtoA, } } diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs index 172d806394..83e51206a0 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs @@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator private MatrixCalculator matrixCalculator; private ClutCalculator clutCalculator; + /// + /// Initializes a new instance of the class for an ICC mAB transform. + /// + /// The parsed A-to-B LUT entry. public LutABCalculator(IccLutAToBTagDataEntry entry) { Guard.NotNull(entry, nameof(entry)); this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); - this.type |= CalculationType.AtoB; + this.type = CalculationType.AtoB; } + /// + /// Initializes a new instance of the class for an ICC mBA transform. + /// + /// The parsed B-to-A LUT entry. public LutABCalculator(IccLutBToATagDataEntry entry) { Guard.NotNull(entry, nameof(entry)); this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); - this.type |= CalculationType.BtoA; + this.type = CalculationType.BtoA; } + /// + /// Calculates the transformed value by applying the configured ICC LUT stages in specification order. + /// + /// The input value. + /// The transformed value. public Vector4 Calculate(Vector4 value) { switch (this.type) { - case CalculationType.Full | CalculationType.AtoB: - value = this.curveACalculator.Calculate(value); - value = this.clutCalculator.Calculate(value); - value = this.curveMCalculator.Calculate(value); - value = this.matrixCalculator.Calculate(value); - return this.curveBCalculator.Calculate(value); - - case CalculationType.Full | CalculationType.BtoA: - value = this.curveBCalculator.Calculate(value); - value = this.matrixCalculator.Calculate(value); - value = this.curveMCalculator.Calculate(value); - value = this.clutCalculator.Calculate(value); - return this.curveACalculator.Calculate(value); - - case CalculationType.CurveClut | CalculationType.AtoB: - value = this.curveACalculator.Calculate(value); - value = this.clutCalculator.Calculate(value); - return this.curveBCalculator.Calculate(value); - - case CalculationType.CurveClut | CalculationType.BtoA: - value = this.curveBCalculator.Calculate(value); - value = this.clutCalculator.Calculate(value); - return this.curveACalculator.Calculate(value); - - case CalculationType.CurveMatrix | CalculationType.AtoB: - value = this.curveMCalculator.Calculate(value); - value = this.matrixCalculator.Calculate(value); - return this.curveBCalculator.Calculate(value); - - case CalculationType.CurveMatrix | CalculationType.BtoA: - value = this.curveBCalculator.Calculate(value); - value = this.matrixCalculator.Calculate(value); - return this.curveMCalculator.Calculate(value); - - case CalculationType.SingleCurve | CalculationType.AtoB: - case CalculationType.SingleCurve | CalculationType.BtoA: - return this.curveBCalculator.Calculate(value); + case CalculationType.AtoB: + // ICC mAB order: A, CLUT, M, Matrix, B. + if (this.curveACalculator != null) + { + value = this.curveACalculator.Calculate(value); + } + + if (this.clutCalculator != null) + { + value = this.clutCalculator.Calculate(value); + } + + if (this.curveMCalculator != null) + { + value = this.curveMCalculator.Calculate(value); + } + + if (this.matrixCalculator != null) + { + value = this.matrixCalculator.Calculate(value); + } + + if (this.curveBCalculator != null) + { + value = this.curveBCalculator.Calculate(value); + } + + return value; + + case CalculationType.BtoA: + // ICC mBA order: B, Matrix, M, CLUT, A. + if (this.curveBCalculator != null) + { + value = this.curveBCalculator.Calculate(value); + } + + if (this.matrixCalculator != null) + { + value = this.matrixCalculator.Calculate(value); + } + + if (this.curveMCalculator != null) + { + value = this.curveMCalculator.Calculate(value); + } + + if (this.clutCalculator != null) + { + value = this.clutCalculator.Calculate(value); + } + + if (this.curveACalculator != null) + { + value = this.curveACalculator.Calculate(value); + } + + return value; default: throw new InvalidOperationException("Invalid calculation type"); } } + /// + /// Creates calculators for the processing stages present in the LUT entry. + /// + /// + /// The tag entry classes already validate channel continuity, so this method only materializes the available stages. + /// private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut) { bool hasACurve = curveA != null; @@ -86,26 +125,10 @@ internal partial class LutABCalculator : IVector4Calculator bool hasMatrix = matrix3x1 != null && matrix3x3 != null; bool hasClut = clut != null; - if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve) - { - this.type = CalculationType.Full; - } - else if (hasBCurve && hasClut && hasACurve) - { - this.type = CalculationType.CurveClut; - } - else if (hasBCurve && hasMatrix && hasMCurve) - { - this.type = CalculationType.CurveMatrix; - } - else if (hasBCurve) - { - this.type = CalculationType.SingleCurve; - } - else - { - throw new InvalidIccProfileException("AToB or BToA tag has an invalid configuration"); - } + Guard.IsTrue( + hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut, + nameof(curveB), + "AToB or BToA tag must contain at least one processing element"); if (hasACurve) { diff --git a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs b/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs index 20df08e378..5875b74f13 100644 --- a/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs +++ b/src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs @@ -60,7 +60,7 @@ internal abstract partial class IccConverterBase IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16), IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB), IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA), - _ => throw new InvalidIccProfileException("Invalid entry."), + _ => throw new InvalidIccProfileException($"Invalid entry {tag}."), }; private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs index 9e89d24ff4..2f1b15b6b7 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs @@ -20,82 +20,46 @@ internal sealed partial class IccDataReader public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info) { this.currentIndex = (int)info.Offset; - switch (this.ReadTagDataEntryHeader()) - { - case IccTypeSignature.Chromaticity: - return this.ReadChromaticityTagDataEntry(); - case IccTypeSignature.ColorantOrder: - return this.ReadColorantOrderTagDataEntry(); - case IccTypeSignature.ColorantTable: - return this.ReadColorantTableTagDataEntry(); - case IccTypeSignature.Curve: - return this.ReadCurveTagDataEntry(); - case IccTypeSignature.Data: - return this.ReadDataTagDataEntry(info.DataSize); - case IccTypeSignature.DateTime: - return this.ReadDateTimeTagDataEntry(); - case IccTypeSignature.Lut16: - return this.ReadLut16TagDataEntry(); - case IccTypeSignature.Lut8: - return this.ReadLut8TagDataEntry(); - case IccTypeSignature.LutAToB: - return this.ReadLutAtoBTagDataEntry(); - case IccTypeSignature.LutBToA: - return this.ReadLutBtoATagDataEntry(); - case IccTypeSignature.Measurement: - return this.ReadMeasurementTagDataEntry(); - case IccTypeSignature.MultiLocalizedUnicode: - return this.ReadMultiLocalizedUnicodeTagDataEntry(); - case IccTypeSignature.MultiProcessElements: - return this.ReadMultiProcessElementsTagDataEntry(); - case IccTypeSignature.NamedColor2: - return this.ReadNamedColor2TagDataEntry(); - case IccTypeSignature.ParametricCurve: - return this.ReadParametricCurveTagDataEntry(); - case IccTypeSignature.ProfileSequenceDesc: - return this.ReadProfileSequenceDescTagDataEntry(); - case IccTypeSignature.ProfileSequenceIdentifier: - return this.ReadProfileSequenceIdentifierTagDataEntry(); - case IccTypeSignature.ResponseCurveSet16: - return this.ReadResponseCurveSet16TagDataEntry(); - case IccTypeSignature.S15Fixed16Array: - return this.ReadFix16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.Signature: - return this.ReadSignatureTagDataEntry(); - case IccTypeSignature.Text: - return this.ReadTextTagDataEntry(info.DataSize); - case IccTypeSignature.U16Fixed16Array: - return this.ReadUFix16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt16Array: - return this.ReadUInt16ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt32Array: - return this.ReadUInt32ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt64Array: - return this.ReadUInt64ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.UInt8Array: - return this.ReadUInt8ArrayTagDataEntry(info.DataSize); - case IccTypeSignature.ViewingConditions: - return this.ReadViewingConditionsTagDataEntry(); - case IccTypeSignature.Xyz: - return this.ReadXyzTagDataEntry(info.DataSize); + return this.ReadTagDataEntryHeader() switch + { + IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(), + IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(), + IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(), + IccTypeSignature.Curve => this.ReadCurveTagDataEntry(), + IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize), + IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(), + IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(), + IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(), + IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(), + IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(), + IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(), + IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(), + IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(), + IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(), + IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(), + IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(), + IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(), + IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(), + IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(), + IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize), + IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize), + IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize), + IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(), + IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize), // V2 Types: - case IccTypeSignature.TextDescription: - return this.ReadTextDescriptionTagDataEntry(); - case IccTypeSignature.CrdInfo: - return this.ReadCrdInfoTagDataEntry(); - case IccTypeSignature.Screening: - return this.ReadScreeningTagDataEntry(); - case IccTypeSignature.UcrBg: - return this.ReadUcrBgTagDataEntry(info.DataSize); + IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(), + IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(), + IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(), + IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize), // Unsupported or unknown - case IccTypeSignature.DeviceSettings: - case IccTypeSignature.NamedColor: - case IccTypeSignature.Unknown: - default: - return this.ReadUnknownTagDataEntry(info.DataSize); - } + _ => this.ReadUnknownTagDataEntry(info.DataSize), + }; } /// @@ -477,7 +441,7 @@ internal sealed partial class IccDataReader return new IccMultiLocalizedUnicodeTagDataEntry(text); - CultureInfo ReadCulture(string language, string country) + static CultureInfo ReadCulture(string language, string country) { if (string.IsNullOrWhiteSpace(language)) { diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs index 05be3eb5dd..eaba0a045c 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs @@ -110,8 +110,8 @@ public sealed partial class IccProfile : IDeepCloneable // need to copy some values because they need to be zero for the hashing Span temp = stackalloc byte[24]; data.AsSpan(profileFlagPos, 4).CopyTo(temp); - data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4)); - data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8)); + data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]); + data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]); try { @@ -131,7 +131,7 @@ public sealed partial class IccProfile : IDeepCloneable } finally { - temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos)); + temp[..4].CopyTo(data.AsSpan(profileFlagPos)); temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos)); temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos)); } diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs index 9bf3232633..2219c4ca06 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs @@ -64,44 +64,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable @@ -192,6 +155,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Compares two curve arrays, treating consistently. + /// private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) { bool thisNull = thisCurves is null; @@ -210,27 +176,63 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null - && this.Matrix3x3 != null - && this.Matrix3x1 != null - && this.CurveM != null - && this.ClutValues != null - && this.CurveA != null; + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mAB order: A, CLUT, M, Matrix, B. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mAB stages: A, CLUT, M, Matrix, and B. + List<(int Input, int Output, string Name)> stages = new(5); - private bool IsMMatrixB() - => this.CurveB != null - && this.Matrix3x3 != null - && this.Matrix3x1 != null - && this.CurveM != null; + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } - private bool IsAClutB() - => this.CurveB != null - && this.ClutValues != null - && this.CurveA != null; + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } - private bool IsB() => this.CurveB != null; + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "AToB tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// private void VerifyCurve(IccTagDataEntry[] curves, string name) { if (curves != null) @@ -240,6 +242,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Verifies the dimensions of the optional matrix components. + /// private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) { if (matrix3x1 != null) @@ -254,6 +259,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Creates the one-dimensional matrix vector when present. + /// private static Vector3? CreateMatrix3x1(float[] matrix) { if (matrix is null) @@ -264,6 +272,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable + /// Creates the three-by-three matrix when present. + /// private static Matrix4x4? CreateMatrix3x3(float[,] matrix) { if (matrix is null) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs index 033b809894..2df1df1378 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs @@ -64,44 +64,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable @@ -191,6 +154,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Compares two curve arrays, treating consistently. + /// private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) { bool thisNull = thisCurves is null; @@ -209,17 +175,63 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null && this.ClutValues != null && this.CurveA != null; + /// + /// Validates the configured processing stages and derives the external channel counts. + /// + /// + /// Stages are evaluated in ICC mBA order: B, Matrix, M, CLUT, A. + /// Sparse pipelines are valid as long as adjacent stages agree on channel counts. + /// + private (int InputChannelCount, int OutputChannelCount) GetChannelCounts() + { + // There are at most five possible mBA stages: B, Matrix, M, CLUT, and A. + List<(int Input, int Output, string Name)> stages = new(5); - private bool IsBMatrixM() - => this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != null; + if (this.CurveB != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB)); + stages.Add((this.CurveB.Length, this.CurveB.Length, nameof(this.CurveB))); + } - private bool IsBClutA() - => this.CurveB != null && this.ClutValues != null && this.CurveA != null; + if (this.Matrix3x3 != null || this.Matrix3x1 != null) + { + Guard.IsTrue(this.Matrix3x3 != null && this.Matrix3x1 != null, nameof(this.Matrix3x3), "Matrix must include both the 3x3 and 3x1 components"); + stages.Add((3, 3, nameof(this.Matrix3x3))); + } - private bool IsB() => this.CurveB != null; + if (this.CurveM != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveM.Length, 1, 15, nameof(this.CurveM)); + stages.Add((this.CurveM.Length, this.CurveM.Length, nameof(this.CurveM))); + } + + if (this.ClutValues != null) + { + stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues))); + } + if (this.CurveA != null) + { + Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA)); + stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA))); + } + + Guard.IsTrue(stages.Count > 0, nameof(this.CurveB), "BToA tag must contain at least one processing element"); + + for (int i = 1; i < stages.Count; i++) + { + Guard.IsTrue( + stages[i - 1].Output == stages[i].Input, + stages[i].Name, + $"Output channel count of {stages[i - 1].Name} does not match input channel count of {stages[i].Name}"); + } + + return (stages[0].Input, stages[^1].Output); + } + + /// + /// Verifies that every supplied curve entry is a supported one-dimensional curve type. + /// private void VerifyCurve(IccTagDataEntry[] curves, string name) { if (curves != null) @@ -229,6 +241,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Verifies the dimensions of the optional matrix components. + /// private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) { if (matrix3x1 != null) @@ -243,6 +258,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Creates the one-dimensional matrix vector when present. + /// private static Vector3? CreateMatrix3x1(float[] matrix) { if (matrix is null) @@ -253,6 +271,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable + /// Creates the three-by-three matrix when present. + /// private static Matrix4x4? CreateMatrix3x3(float[,] matrix) { if (matrix is null) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 3fd55eb915..d047ed2357 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -418,6 +418,21 @@ public partial class JpegDecoderTests image.CompareToReferenceOutput(provider); } + [Theory] + [WithFile(TestImages.Jpeg.ICC.Issue3064, PixelTypes.Rgba32)] + public void Decode_RGB_ICC_Jpeg_Issue3064(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + JpegDecoderOptions options = new() + { + GeneralOptions = new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert } + }; + + using Image image = provider.GetImage(JpegDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + // https://github.com/SixLabors/ImageSharp/issues/2948 [Theory] [WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 764954cae0..fab1b2891c 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -232,6 +232,7 @@ public static class TestImages public const string SRgbGray = "Jpg/icc-profiles/sRGB_Gray.jpg"; public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg"; public const string PerceptualcLUTOnly = "Jpg/icc-profiles/Perceptual-cLUT-only.jpg"; + public const string Issue3064 = "Jpg/icc-profiles/issue-3064.jpg"; } public static class Progressive diff --git a/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png new file mode 100644 index 0000000000..12692cb9ea --- /dev/null +++ b/tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73497e1eaddaa86cc915fe59a177e9ab6423d725ab4e6af21c8414c9edc6ceaf +size 347 diff --git a/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg b/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg new file mode 100644 index 0000000000..477372c033 --- /dev/null +++ b/tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e02fff450519423fd5746e610d65bd7296553252567e93de9c051250139e8adc +size 27537 From 4fc4d660fadccb4737528319e625a82f0cc23ea4 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 6 Mar 2026 23:02:20 +1000 Subject: [PATCH 6/8] Respond to feedback --- .../ColorProfiles/Icc/Calculators/LutABCalculator.cs | 2 +- .../Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs | 4 ++-- .../Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs index 83e51206a0..10ac6e596f 100644 --- a/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs +++ b/src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs @@ -127,7 +127,7 @@ internal partial class LutABCalculator : IVector4Calculator Guard.IsTrue( hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut, - nameof(curveB), + "entry", "AToB or BToA tag must contain at least one processing element"); if (hasACurve) diff --git a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs index 2219c4ca06..77bc45bd4f 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs @@ -128,7 +128,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable Date: Sat, 7 Mar 2026 10:52:14 +1000 Subject: [PATCH 7/8] JPEG - Throw explicit ImageContentException on missing marker. --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 5 +++++ tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 7825955e7a..d4517e9f19 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -519,6 +519,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData fileMarker = FindNextFileMarker(stream); } + if (!metadataOnly && this.Frame is null) + { + JpegThrowHelper.ThrowInvalidImageContentException("No readable SOFn (Start Of Frame) marker found."); + } + this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved; } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index d047ed2357..36847536b3 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -448,4 +448,13 @@ public partial class JpegDecoderTests [InlineData(TestImages.Jpeg.Issues.Issue2948)] public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath) => Assert.Throws(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes)); + + [Fact] + public void Issue_3071_Decode_TruncatedJpeg_Throws_InvalidImageContentException() + => Assert.Throws(() => + { + // SOI marker (FF D8) + garbage bytes — only 11 bytes + byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30]; + using Image image = Image.Load(data); + }); } From dd827571d1ad31aab9e0492f2f8413230c6ac3ee Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 7 Mar 2026 12:27:06 +1000 Subject: [PATCH 8/8] Replace AntialiasSubpixelDepth with AntialiasThreshold --- src/ImageSharp/GraphicsOptions.cs | 46 ++++++++++--------- .../ImageSharp.Tests/GraphicsOptionsTests.cs | 32 ++++++------- .../TestUtilities/GraphicsOptionsComparer.cs | 12 ++--- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/src/ImageSharp/GraphicsOptions.cs b/src/ImageSharp/GraphicsOptions.cs index dc3d179027..e056596512 100644 --- a/src/ImageSharp/GraphicsOptions.cs +++ b/src/ImageSharp/GraphicsOptions.cs @@ -6,11 +6,12 @@ using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; /// -/// Options for influencing the drawing functions. +/// Provides configuration for controlling how graphics operations are rendered, +/// including antialiasing, pixel blending, alpha composition, and coverage thresholding. /// public class GraphicsOptions : IDeepCloneable { - private int antialiasSubpixelDepth = 16; + private float antialiasThreshold = .5F; private float blendPercentage = 1F; /// @@ -24,61 +25,62 @@ public class GraphicsOptions : IDeepCloneable { this.AlphaCompositionMode = source.AlphaCompositionMode; this.Antialias = source.Antialias; - this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; + this.AntialiasThreshold = source.AntialiasThreshold; this.BlendPercentage = source.BlendPercentage; this.ColorBlendingMode = source.ColorBlendingMode; } /// /// Gets or sets a value indicating whether antialiasing should be applied. - /// Defaults to true. + /// When , edges are rendered with smooth sub-pixel coverage. + /// When , coverage is snapped to binary (fully opaque or fully transparent) + /// using as the cutoff. + /// Defaults to . /// public bool Antialias { get; set; } = true; /// - /// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. - /// Defaults to 16. + /// Gets or sets the coverage threshold used when is . + /// Pixels with antialiased coverage above this value are rendered as fully opaque; + /// pixels below are discarded. Valid range is 0 to 1. Lower values preserve more + /// thin features at small sizes. Defaults to 0.5F. /// - public int AntialiasSubpixelDepth + public float AntialiasThreshold { - get - { - return this.antialiasSubpixelDepth; - } + get => this.antialiasThreshold; set { - Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); - this.antialiasSubpixelDepth = value; + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold)); + this.antialiasThreshold = value; } } /// - /// Gets or sets a value between indicating the blending percentage to apply to the drawing operation. - /// Range 0..1; Defaults to 1. + /// Gets or sets the blending percentage applied to the drawing operation. + /// A value of 1.0 applies the operation at full strength; 0.0 makes it invisible. + /// Valid range is 0 to 1. Defaults to 1.0F. /// public float BlendPercentage { - get - { - return this.blendPercentage; - } + get => this.blendPercentage; set { - Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); + Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage)); this.blendPercentage = value; } } /// - /// Gets or sets a value indicating the color blending mode to apply to the drawing operation. + /// Gets or sets the color blending mode used to combine source and destination pixel colors. /// Defaults to . /// public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; /// - /// Gets or sets a value indicating the alpha composition mode to apply to the drawing operation + /// Gets or sets the alpha composition mode that determines how source and destination alpha + /// channels are combined using Porter-Duff operators. /// Defaults to . /// public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; diff --git a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs index 0ccb80d3f5..1b87819b68 100644 --- a/tests/ImageSharp.Tests/GraphicsOptionsTests.cs +++ b/tests/ImageSharp.Tests/GraphicsOptionsTests.cs @@ -13,7 +13,7 @@ public class GraphicsOptionsTests private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); [Fact] - public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); + public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions); [Fact] public void DefaultGraphicsOptionsAntialias() @@ -23,35 +23,35 @@ public class GraphicsOptionsTests } [Fact] - public void DefaultGraphicsOptionsAntialiasSuppixelDepth() + public void DefaultGraphicsOptionsAntialiasThreshold() { - const int Expected = 16; - Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); - Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); + const float expected = .5F; + Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold); + Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold); } [Fact] public void DefaultGraphicsOptionsBlendPercentage() { - const float Expected = 1F; - Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); - Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); + const float expected = 1F; + Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage); + Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage); } [Fact] public void DefaultGraphicsOptionsColorBlendingMode() { - const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; - Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); + const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal; + Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode); + Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode); } [Fact] public void DefaultGraphicsOptionsAlphaCompositionMode() { - const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; - Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); - Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); + const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver; + Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode); + Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode); } [Fact] @@ -61,7 +61,7 @@ public class GraphicsOptionsTests { AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, Antialias = false, - AntialiasSubpixelDepth = 23, + AntialiasThreshold = .33F, BlendPercentage = .25F, ColorBlendingMode = PixelColorBlendingMode.HardLight, }; @@ -79,7 +79,7 @@ public class GraphicsOptionsTests actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.Antialias = false; - actual.AntialiasSubpixelDepth = 23; + actual.AntialiasThreshold = .67F; actual.BlendPercentage = .25F; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; diff --git a/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs index 2a7b42f6b4..649da425ec 100644 --- a/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs @@ -6,13 +6,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities; public class GraphicsOptionsComparer : IEqualityComparer { public bool Equals(GraphicsOptions x, GraphicsOptions y) - { - return x.AlphaCompositionMode == y.AlphaCompositionMode - && x.Antialias == y.Antialias - && x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth - && x.BlendPercentage == y.BlendPercentage - && x.ColorBlendingMode == y.ColorBlendingMode; - } + => x.AlphaCompositionMode == y.AlphaCompositionMode + && x.Antialias == y.Antialias + && x.AntialiasThreshold == y.AntialiasThreshold + && x.BlendPercentage == y.BlendPercentage + && x.ColorBlendingMode == y.ColorBlendingMode; public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode(); }