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..10ac6e596f 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,
+ "entry",
+ "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/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
index 17c1f545b7..a1de790c3d 100644
--- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
+++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
@@ -131,6 +131,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
try
{
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);
@@ -138,23 +139,27 @@ 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 > 0 and <= 8 && this.processedAlphaMask:
this.ReadRgbPaletteWithAlphaMask(
stream,
pixels,
@@ -166,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
inverted);
break;
- case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8:
+
+ case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8:
this.ReadRgbPalette(
stream,
pixels,
@@ -179,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);
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/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/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..77bc45bd4f 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
@@ -165,7 +128,7 @@ 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;
@@ -202,7 +168,7 @@ 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..37e7b408d2 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
@@ -165,7 +128,7 @@ 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;
@@ -201,7 +167,7 @@ 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/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 94cfe85ee5..6ebe1bf4e0 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);
+
+ Assert.Throws(() =>
+ {
+ using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream);
+ });
+ }
}
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);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 3fd55eb915..36847536b3 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)]
@@ -433,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);
+ });
}
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/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/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();
}
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