Browse Source

Merge branch 'main' into dependabot/github_actions/actions/checkout-6

pull/3057/head
James Jackson-South 2 months ago
committed by GitHub
parent
commit
6bb36244c6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs
  2. 141
      src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
  3. 2
      src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs
  4. 22
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  5. 5
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  6. 46
      src/ImageSharp/GraphicsOptions.cs
  7. 110
      src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs
  8. 6
      src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs
  9. 125
      src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs
  10. 115
      src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs
  11. 23
      tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
  12. 11
      tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs
  13. 24
      tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
  14. 32
      tests/ImageSharp.Tests/GraphicsOptionsTests.cs
  15. 1
      tests/ImageSharp.Tests/TestImages.cs
  16. 12
      tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs
  17. 3
      tests/Images/External/ReferenceOutput/JpegDecoderTests/Decode_RGB_ICC_Jpeg_Issue3064_Rgba32_issue-3064.png
  18. 3
      tests/Images/Input/Jpg/icc-profiles/issue-3064.jpg

17
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.CalculationType.cs

@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
internal partial class LutABCalculator internal partial class LutABCalculator
{ {
/// <summary>
/// Identifies the transform direction for the configured LUT calculator.
/// </summary>
private enum CalculationType private enum CalculationType
{ {
AtoB = 1 << 3, /// <summary>
BtoA = 1 << 4, /// Converts from device space to PCS using ICC <c>mAB</c> stage order.
/// </summary>
AtoB,
SingleCurve = 1, /// <summary>
CurveMatrix = 2, /// Converts from PCS to device space using ICC <c>mBA</c> stage order.
CurveClut = 3, /// </summary>
Full = 4, BtoA,
} }
} }

141
src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs

@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator
private MatrixCalculator matrixCalculator; private MatrixCalculator matrixCalculator;
private ClutCalculator clutCalculator; private ClutCalculator clutCalculator;
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
/// </summary>
/// <param name="entry">The parsed A-to-B LUT entry.</param>
public LutABCalculator(IccLutAToBTagDataEntry entry) public LutABCalculator(IccLutAToBTagDataEntry entry)
{ {
Guard.NotNull(entry, nameof(entry)); Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.AtoB; this.type = CalculationType.AtoB;
} }
/// <summary>
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
/// </summary>
/// <param name="entry">The parsed B-to-A LUT entry.</param>
public LutABCalculator(IccLutBToATagDataEntry entry) public LutABCalculator(IccLutBToATagDataEntry entry)
{ {
Guard.NotNull(entry, nameof(entry)); Guard.NotNull(entry, nameof(entry));
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues); this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
this.type |= CalculationType.BtoA; this.type = CalculationType.BtoA;
} }
/// <summary>
/// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
/// </summary>
/// <param name="value">The input value.</param>
/// <returns>The transformed value.</returns>
public Vector4 Calculate(Vector4 value) public Vector4 Calculate(Vector4 value)
{ {
switch (this.type) switch (this.type)
{ {
case CalculationType.Full | CalculationType.AtoB: case CalculationType.AtoB:
value = this.curveACalculator.Calculate(value); // ICC mAB order: A, CLUT, M, Matrix, B.
value = this.clutCalculator.Calculate(value); if (this.curveACalculator != null)
value = this.curveMCalculator.Calculate(value); {
value = this.matrixCalculator.Calculate(value); value = this.curveACalculator.Calculate(value);
return this.curveBCalculator.Calculate(value); }
case CalculationType.Full | CalculationType.BtoA: if (this.clutCalculator != null)
value = this.curveBCalculator.Calculate(value); {
value = this.matrixCalculator.Calculate(value); value = this.clutCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value); }
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value); if (this.curveMCalculator != null)
{
case CalculationType.CurveClut | CalculationType.AtoB: value = this.curveMCalculator.Calculate(value);
value = this.curveACalculator.Calculate(value); }
value = this.clutCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value); if (this.matrixCalculator != null)
{
case CalculationType.CurveClut | CalculationType.BtoA: value = this.matrixCalculator.Calculate(value);
value = this.curveBCalculator.Calculate(value); }
value = this.clutCalculator.Calculate(value);
return this.curveACalculator.Calculate(value); if (this.curveBCalculator != null)
{
case CalculationType.CurveMatrix | CalculationType.AtoB: value = this.curveBCalculator.Calculate(value);
value = this.curveMCalculator.Calculate(value); }
value = this.matrixCalculator.Calculate(value);
return this.curveBCalculator.Calculate(value); return value;
case CalculationType.CurveMatrix | CalculationType.BtoA: case CalculationType.BtoA:
value = this.curveBCalculator.Calculate(value); // ICC mBA order: B, Matrix, M, CLUT, A.
value = this.matrixCalculator.Calculate(value); if (this.curveBCalculator != null)
return this.curveMCalculator.Calculate(value); {
value = this.curveBCalculator.Calculate(value);
case CalculationType.SingleCurve | CalculationType.AtoB: }
case CalculationType.SingleCurve | CalculationType.BtoA:
return 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: default:
throw new InvalidOperationException("Invalid calculation type"); throw new InvalidOperationException("Invalid calculation type");
} }
} }
/// <summary>
/// Creates calculators for the processing stages present in the LUT entry.
/// </summary>
/// <remarks>
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
/// </remarks>
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut) private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
{ {
bool hasACurve = curveA != null; bool hasACurve = curveA != null;
@ -86,26 +125,10 @@ internal partial class LutABCalculator : IVector4Calculator
bool hasMatrix = matrix3x1 != null && matrix3x3 != null; bool hasMatrix = matrix3x1 != null && matrix3x3 != null;
bool hasClut = clut != null; bool hasClut = clut != null;
if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve) Guard.IsTrue(
{ hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut,
this.type = CalculationType.Full; "entry",
} "AToB or BToA tag must contain at least one processing element");
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");
}
if (hasACurve) if (hasACurve)
{ {

2
src/ImageSharp/ColorProfiles/Icc/IccConverterbase.Conversions.cs

@ -60,7 +60,7 @@ internal abstract partial class IccConverterBase
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16), IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB), IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA), IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
_ => throw new InvalidIccProfileException("Invalid entry."), _ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
}; };
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag) private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)

22
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -131,6 +131,7 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
try try
{ {
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
ushort bitsPerPixel = this.infoHeader.BitsPerPixel;
image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata); image = new Image<TPixel>(this.configuration, this.infoHeader.Width, this.infoHeader.Height, this.metadata);
@ -138,23 +139,27 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
switch (this.infoHeader.Compression) 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); this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; 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); this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; 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); this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; 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); this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; 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( this.ReadRgbPaletteWithAlphaMask(
stream, stream,
pixels, pixels,
@ -166,7 +171,8 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
inverted); inverted);
break; break;
case BmpCompression.RGB when this.infoHeader.BitsPerPixel is <= 8:
case BmpCompression.RGB when bitsPerPixel is > 0 and <= 8:
this.ReadRgbPalette( this.ReadRgbPalette(
stream, stream,
pixels, pixels,
@ -179,6 +185,10 @@ internal sealed class BmpDecoderCore : ImageDecoderCore
break; break;
case BmpCompression.RGB when bitsPerPixel is <= 0 or > 32:
BmpThrowHelper.ThrowInvalidImageContentException($"Invalid bits per pixel: {bitsPerPixel}");
break;
case BmpCompression.RLE24: case BmpCompression.RLE24:
this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);

5
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -519,6 +519,11 @@ internal sealed class JpegDecoderCore : ImageDecoderCore, IRawJpegData
fileMarker = FindNextFileMarker(stream); 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; this.Metadata.GetJpegMetadata().Interleaved = this.Frame.Interleaved;
} }

46
src/ImageSharp/GraphicsOptions.cs

@ -6,11 +6,12 @@ using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp; namespace SixLabors.ImageSharp;
/// <summary> /// <summary>
/// Options for influencing the drawing functions. /// Provides configuration for controlling how graphics operations are rendered,
/// including antialiasing, pixel blending, alpha composition, and coverage thresholding.
/// </summary> /// </summary>
public class GraphicsOptions : IDeepCloneable<GraphicsOptions> public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{ {
private int antialiasSubpixelDepth = 16; private float antialiasThreshold = .5F;
private float blendPercentage = 1F; private float blendPercentage = 1F;
/// <summary> /// <summary>
@ -24,61 +25,62 @@ public class GraphicsOptions : IDeepCloneable<GraphicsOptions>
{ {
this.AlphaCompositionMode = source.AlphaCompositionMode; this.AlphaCompositionMode = source.AlphaCompositionMode;
this.Antialias = source.Antialias; this.Antialias = source.Antialias;
this.AntialiasSubpixelDepth = source.AntialiasSubpixelDepth; this.AntialiasThreshold = source.AntialiasThreshold;
this.BlendPercentage = source.BlendPercentage; this.BlendPercentage = source.BlendPercentage;
this.ColorBlendingMode = source.ColorBlendingMode; this.ColorBlendingMode = source.ColorBlendingMode;
} }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether antialiasing should be applied. /// Gets or sets a value indicating whether antialiasing should be applied.
/// Defaults to true. /// When <see langword="true"/>, edges are rendered with smooth sub-pixel coverage.
/// When <see langword="false"/>, coverage is snapped to binary (fully opaque or fully transparent)
/// using <see cref="AntialiasThreshold"/> as the cutoff.
/// Defaults to <see langword="true"/>.
/// </summary> /// </summary>
public bool Antialias { get; set; } = true; public bool Antialias { get; set; } = true;
/// <summary> /// <summary>
/// Gets or sets a value indicating the number of subpixels to use while rendering with antialiasing enabled. /// Gets or sets the coverage threshold used when <see cref="Antialias"/> is <see langword="false"/>.
/// Defaults to 16. /// 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 <c>0.5F</c>.
/// </summary> /// </summary>
public int AntialiasSubpixelDepth public float AntialiasThreshold
{ {
get get => this.antialiasThreshold;
{
return this.antialiasSubpixelDepth;
}
set set
{ {
Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.AntialiasSubpixelDepth)); Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.AntialiasThreshold));
this.antialiasSubpixelDepth = value; this.antialiasThreshold = value;
} }
} }
/// <summary> /// <summary>
/// Gets or sets a value between indicating the blending percentage to apply to the drawing operation. /// Gets or sets the blending percentage applied to the drawing operation.
/// Range 0..1; Defaults to 1. /// A value of <c>1.0</c> applies the operation at full strength; <c>0.0</c> makes it invisible.
/// Valid range is 0 to 1. Defaults to <c>1.0F</c>.
/// </summary> /// </summary>
public float BlendPercentage public float BlendPercentage
{ {
get get => this.blendPercentage;
{
return this.blendPercentage;
}
set set
{ {
Guard.MustBeBetweenOrEqualTo(value, 0, 1F, nameof(this.BlendPercentage)); Guard.MustBeBetweenOrEqualTo(value, 0F, 1F, nameof(this.BlendPercentage));
this.blendPercentage = value; this.blendPercentage = value;
} }
} }
/// <summary> /// <summary>
/// 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 <see cref="PixelColorBlendingMode.Normal"/>. /// Defaults to <see cref="PixelColorBlendingMode.Normal"/>.
/// </summary> /// </summary>
public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal; public PixelColorBlendingMode ColorBlendingMode { get; set; } = PixelColorBlendingMode.Normal;
/// <summary> /// <summary>
/// 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 <see cref="PixelAlphaCompositionMode.SrcOver"/>. /// Defaults to <see cref="PixelAlphaCompositionMode.SrcOver"/>.
/// </summary> /// </summary>
public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver; public PixelAlphaCompositionMode AlphaCompositionMode { get; set; } = PixelAlphaCompositionMode.SrcOver;

110
src/ImageSharp/Metadata/Profiles/ICC/DataReader/IccDataReader.TagDataEntry.cs

@ -20,82 +20,46 @@ internal sealed partial class IccDataReader
public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info) public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info)
{ {
this.currentIndex = (int)info.Offset; this.currentIndex = (int)info.Offset;
switch (this.ReadTagDataEntryHeader()) return this.ReadTagDataEntryHeader() switch
{ {
case IccTypeSignature.Chromaticity: IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(),
return this.ReadChromaticityTagDataEntry(); IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(),
case IccTypeSignature.ColorantOrder: IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(),
return this.ReadColorantOrderTagDataEntry(); IccTypeSignature.Curve => this.ReadCurveTagDataEntry(),
case IccTypeSignature.ColorantTable: IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize),
return this.ReadColorantTableTagDataEntry(); IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(),
case IccTypeSignature.Curve: IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(),
return this.ReadCurveTagDataEntry(); IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(),
case IccTypeSignature.Data: IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(),
return this.ReadDataTagDataEntry(info.DataSize); IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(),
case IccTypeSignature.DateTime: IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(),
return this.ReadDateTimeTagDataEntry(); IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(),
case IccTypeSignature.Lut16: IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(),
return this.ReadLut16TagDataEntry(); IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(),
case IccTypeSignature.Lut8: IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(),
return this.ReadLut8TagDataEntry(); IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(),
case IccTypeSignature.LutAToB: IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(),
return this.ReadLutAtoBTagDataEntry(); IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(),
case IccTypeSignature.LutBToA: IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize),
return this.ReadLutBtoATagDataEntry(); IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(),
case IccTypeSignature.Measurement: IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize),
return this.ReadMeasurementTagDataEntry(); IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize),
case IccTypeSignature.MultiLocalizedUnicode: IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize),
return this.ReadMultiLocalizedUnicodeTagDataEntry(); IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize),
case IccTypeSignature.MultiProcessElements: IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize),
return this.ReadMultiProcessElementsTagDataEntry(); IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize),
case IccTypeSignature.NamedColor2: IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(),
return this.ReadNamedColor2TagDataEntry(); IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize),
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);
// V2 Types: // V2 Types:
case IccTypeSignature.TextDescription: IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(),
return this.ReadTextDescriptionTagDataEntry(); IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(),
case IccTypeSignature.CrdInfo: IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(),
return this.ReadCrdInfoTagDataEntry(); IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize),
case IccTypeSignature.Screening:
return this.ReadScreeningTagDataEntry();
case IccTypeSignature.UcrBg:
return this.ReadUcrBgTagDataEntry(info.DataSize);
// Unsupported or unknown // Unsupported or unknown
case IccTypeSignature.DeviceSettings: _ => this.ReadUnknownTagDataEntry(info.DataSize),
case IccTypeSignature.NamedColor: };
case IccTypeSignature.Unknown:
default:
return this.ReadUnknownTagDataEntry(info.DataSize);
}
} }
/// <summary> /// <summary>
@ -477,7 +441,7 @@ internal sealed partial class IccDataReader
return new IccMultiLocalizedUnicodeTagDataEntry(text); return new IccMultiLocalizedUnicodeTagDataEntry(text);
CultureInfo ReadCulture(string language, string country) static CultureInfo ReadCulture(string language, string country)
{ {
if (string.IsNullOrWhiteSpace(language)) if (string.IsNullOrWhiteSpace(language))
{ {

6
src/ImageSharp/Metadata/Profiles/ICC/IccProfile.cs

@ -110,8 +110,8 @@ public sealed partial class IccProfile : IDeepCloneable<IccProfile>
// need to copy some values because they need to be zero for the hashing // need to copy some values because they need to be zero for the hashing
Span<byte> temp = stackalloc byte[24]; Span<byte> temp = stackalloc byte[24];
data.AsSpan(profileFlagPos, 4).CopyTo(temp); data.AsSpan(profileFlagPos, 4).CopyTo(temp);
data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4)); data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]);
data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8)); data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]);
try try
{ {
@ -131,7 +131,7 @@ public sealed partial class IccProfile : IDeepCloneable<IccProfile>
} }
finally 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(4, 4).CopyTo(data.AsSpan(renderingIntentPos));
temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos)); temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos));
} }

125
src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutAToBTagDataEntry.cs

@ -64,44 +64,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
this.CurveM = curveM; this.CurveM = curveM;
this.ClutValues = clutValues; this.ClutValues = clutValues;
if (this.IsAClutMMatrixB()) (this.InputChannelCount, this.OutputChannelCount) = this.GetChannelCounts();
{
Guard.IsTrue(this.CurveB.Length == 3, nameof(this.CurveB), $"{nameof(this.CurveB)} must have a length of three");
Guard.IsTrue(this.CurveM.Length == 3, nameof(this.CurveM), $"{nameof(this.CurveM)} must have a length of three");
Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
this.InputChannelCount = curveA.Length;
this.OutputChannelCount = 3;
Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size");
Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size");
}
else if (this.IsMMatrixB())
{
Guard.IsTrue(this.CurveB.Length == 3, nameof(this.CurveB), $"{nameof(this.CurveB)} must have a length of three");
Guard.IsTrue(this.CurveM.Length == 3, nameof(this.CurveM), $"{nameof(this.CurveM)} must have a length of three");
this.InputChannelCount = this.OutputChannelCount = 3;
}
else if (this.IsAClutB())
{
Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB));
this.InputChannelCount = curveA.Length;
this.OutputChannelCount = curveB.Length;
Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size");
Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size");
}
else if (this.IsB())
{
this.InputChannelCount = this.OutputChannelCount = this.CurveB.Length;
}
else
{
throw new ArgumentException("Invalid combination of values given");
}
} }
/// <summary> /// <summary>
@ -165,7 +128,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
&& this.OutputChannelCount == other.OutputChannelCount && this.OutputChannelCount == other.OutputChannelCount
&& this.Matrix3x3.Equals(other.Matrix3x3) && this.Matrix3x3.Equals(other.Matrix3x3)
&& this.Matrix3x1.Equals(other.Matrix3x1) && this.Matrix3x1.Equals(other.Matrix3x1)
&& this.ClutValues.Equals(other.ClutValues) && Equals(this.ClutValues, other.ClutValues)
&& EqualsCurve(this.CurveB, other.CurveB) && EqualsCurve(this.CurveB, other.CurveB)
&& EqualsCurve(this.CurveM, other.CurveM) && EqualsCurve(this.CurveM, other.CurveM)
&& EqualsCurve(this.CurveA, other.CurveA); && EqualsCurve(this.CurveA, other.CurveA);
@ -192,6 +155,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
return hashCode.ToHashCode(); return hashCode.ToHashCode();
} }
/// <summary>
/// Compares two curve arrays, treating <see langword="null"/> consistently.
/// </summary>
private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves)
{ {
bool thisNull = thisCurves is null; bool thisNull = thisCurves is null;
@ -202,7 +168,7 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
return true; return true;
} }
if (entryNull) if (thisNull || entryNull)
{ {
return false; return false;
} }
@ -210,27 +176,63 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
return thisCurves.SequenceEqual(entryCurves); return thisCurves.SequenceEqual(entryCurves);
} }
private bool IsAClutMMatrixB() /// <summary>
=> this.CurveB != null /// Validates the configured processing stages and derives the external channel counts.
&& this.Matrix3x3 != null /// </summary>
&& this.Matrix3x1 != null /// <remarks>
&& this.CurveM != null /// Stages are evaluated in ICC <c>mAB</c> order: A, CLUT, M, Matrix, B.
&& this.ClutValues != null /// Sparse pipelines are valid as long as adjacent stages agree on channel counts.
&& this.CurveA != null; /// </remarks>
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() if (this.CurveA != null)
=> this.CurveB != null {
&& this.Matrix3x3 != null Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
&& this.Matrix3x1 != null stages.Add((this.CurveA.Length, this.CurveA.Length, nameof(this.CurveA)));
&& this.CurveM != null; }
private bool IsAClutB() if (this.ClutValues != null)
=> this.CurveB != null {
&& this.ClutValues != null stages.Add((this.ClutValues.InputChannelCount, this.ClutValues.OutputChannelCount, nameof(this.ClutValues)));
&& this.CurveA != null; }
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);
}
/// <summary>
/// Verifies that every supplied curve entry is a supported one-dimensional curve type.
/// </summary>
private void VerifyCurve(IccTagDataEntry[] curves, string name) private void VerifyCurve(IccTagDataEntry[] curves, string name)
{ {
if (curves != null) if (curves != null)
@ -240,6 +242,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
} }
} }
/// <summary>
/// Verifies the dimensions of the optional matrix components.
/// </summary>
private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1)
{ {
if (matrix3x1 != null) if (matrix3x1 != null)
@ -254,6 +259,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
} }
} }
/// <summary>
/// Creates the one-dimensional matrix vector when present.
/// </summary>
private static Vector3? CreateMatrix3x1(float[] matrix) private static Vector3? CreateMatrix3x1(float[] matrix)
{ {
if (matrix is null) if (matrix is null)
@ -264,6 +272,9 @@ internal sealed class IccLutAToBTagDataEntry : IccTagDataEntry, IEquatable<IccLu
return new Vector3(matrix[0], matrix[1], matrix[2]); return new Vector3(matrix[0], matrix[1], matrix[2]);
} }
/// <summary>
/// Creates the three-by-three matrix when present.
/// </summary>
private static Matrix4x4? CreateMatrix3x3(float[,] matrix) private static Matrix4x4? CreateMatrix3x3(float[,] matrix)
{ {
if (matrix is null) if (matrix is null)

115
src/ImageSharp/Metadata/Profiles/ICC/TagDataEntries/IccLutBToATagDataEntry.cs

@ -64,44 +64,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
this.CurveM = curveM; this.CurveM = curveM;
this.ClutValues = clutValues; this.ClutValues = clutValues;
if (this.IsBMatrixMClutA()) (this.InputChannelCount, this.OutputChannelCount) = this.GetChannelCounts();
{
Guard.IsTrue(this.CurveB.Length == 3, nameof(this.CurveB), $"{nameof(this.CurveB)} must have a length of three");
Guard.IsTrue(this.CurveM.Length == 3, nameof(this.CurveM), $"{nameof(this.CurveM)} must have a length of three");
Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
this.InputChannelCount = 3;
this.OutputChannelCount = curveA.Length;
Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size");
Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size");
}
else if (this.IsBMatrixM())
{
Guard.IsTrue(this.CurveB.Length == 3, nameof(this.CurveB), $"{nameof(this.CurveB)} must have a length of three");
Guard.IsTrue(this.CurveM.Length == 3, nameof(this.CurveM), $"{nameof(this.CurveM)} must have a length of three");
this.InputChannelCount = this.OutputChannelCount = 3;
}
else if (this.IsBClutA())
{
Guard.MustBeBetweenOrEqualTo(this.CurveA.Length, 1, 15, nameof(this.CurveA));
Guard.MustBeBetweenOrEqualTo(this.CurveB.Length, 1, 15, nameof(this.CurveB));
this.InputChannelCount = curveB.Length;
this.OutputChannelCount = curveA.Length;
Guard.IsTrue(this.InputChannelCount == clutValues.InputChannelCount, nameof(clutValues), "Input channel count does not match the CLUT size");
Guard.IsTrue(this.OutputChannelCount == clutValues.OutputChannelCount, nameof(clutValues), "Output channel count does not match the CLUT size");
}
else if (this.IsB())
{
this.InputChannelCount = this.OutputChannelCount = this.CurveB.Length;
}
else
{
throw new ArgumentException("Invalid combination of values given");
}
} }
/// <summary> /// <summary>
@ -165,7 +128,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
&& this.OutputChannelCount == other.OutputChannelCount && this.OutputChannelCount == other.OutputChannelCount
&& this.Matrix3x3.Equals(other.Matrix3x3) && this.Matrix3x3.Equals(other.Matrix3x3)
&& this.Matrix3x1.Equals(other.Matrix3x1) && this.Matrix3x1.Equals(other.Matrix3x1)
&& this.ClutValues.Equals(other.ClutValues) && Equals(this.ClutValues, other.ClutValues)
&& EqualsCurve(this.CurveB, other.CurveB) && EqualsCurve(this.CurveB, other.CurveB)
&& EqualsCurve(this.CurveM, other.CurveM) && EqualsCurve(this.CurveM, other.CurveM)
&& EqualsCurve(this.CurveA, other.CurveA); && EqualsCurve(this.CurveA, other.CurveA);
@ -191,6 +154,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
return hashCode.ToHashCode(); return hashCode.ToHashCode();
} }
/// <summary>
/// Compares two curve arrays, treating <see langword="null"/> consistently.
/// </summary>
private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves) private static bool EqualsCurve(IccTagDataEntry[] thisCurves, IccTagDataEntry[] entryCurves)
{ {
bool thisNull = thisCurves is null; bool thisNull = thisCurves is null;
@ -201,7 +167,7 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
return true; return true;
} }
if (entryNull) if (thisNull || entryNull)
{ {
return false; return false;
} }
@ -209,17 +175,63 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
return thisCurves.SequenceEqual(entryCurves); return thisCurves.SequenceEqual(entryCurves);
} }
private bool IsBMatrixMClutA() /// <summary>
=> 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.
/// </summary>
/// <remarks>
/// Stages are evaluated in ICC <c>mBA</c> order: B, Matrix, M, CLUT, A.
/// Sparse pipelines are valid as long as adjacent stages agree on channel counts.
/// </remarks>
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() if (this.CurveB != null)
=> this.CurveB != null && this.Matrix3x3 != null && this.Matrix3x1 != null && this.CurveM != 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() if (this.Matrix3x3 != null || this.Matrix3x1 != null)
=> this.CurveB != null && this.ClutValues != null && this.CurveA != 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);
}
/// <summary>
/// Verifies that every supplied curve entry is a supported one-dimensional curve type.
/// </summary>
private void VerifyCurve(IccTagDataEntry[] curves, string name) private void VerifyCurve(IccTagDataEntry[] curves, string name)
{ {
if (curves != null) if (curves != null)
@ -229,6 +241,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
} }
} }
/// <summary>
/// Verifies the dimensions of the optional matrix components.
/// </summary>
private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1) private static void VerifyMatrix(float[,] matrix3x3, float[] matrix3x1)
{ {
if (matrix3x1 != null) if (matrix3x1 != null)
@ -243,6 +258,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
} }
} }
/// <summary>
/// Creates the one-dimensional matrix vector when present.
/// </summary>
private static Vector3? CreateMatrix3x1(float[] matrix) private static Vector3? CreateMatrix3x1(float[] matrix)
{ {
if (matrix is null) if (matrix is null)
@ -253,6 +271,9 @@ internal sealed class IccLutBToATagDataEntry : IccTagDataEntry, IEquatable<IccLu
return new Vector3(matrix[0], matrix[1], matrix[2]); return new Vector3(matrix[0], matrix[1], matrix[2]);
} }
/// <summary>
/// Creates the three-by-three matrix when present.
/// </summary>
private static Matrix4x4? CreateMatrix3x3(float[,] matrix) private static Matrix4x4? CreateMatrix3x3(float[,] matrix)
{ {
if (matrix is null) if (matrix is null)

23
tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs

@ -571,4 +571,27 @@ public class BmpDecoderTests
}); });
Assert.IsType<InvalidMemoryOperationException>(ex.InnerException); Assert.IsType<InvalidMemoryOperationException>(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<InvalidImageContentException>(() =>
{
using Image image = BmpDecoder.Instance.Decode(DecoderOptions.Default, stream);
});
}
} }

11
tests/ImageSharp.Tests/Formats/Icon/Ico/IcoDecoderTests.cs

@ -204,12 +204,19 @@ public class IcoDecoderTests
} }
[Theory] [Theory]
[WithFile(InvalidAll, PixelTypes.Rgba32)]
[WithFile(InvalidBpp, PixelTypes.Rgba32)] [WithFile(InvalidBpp, PixelTypes.Rgba32)]
public void InvalidThrows_InvalidImageContentException(TestImageProvider<Rgba32> provider)
=> Assert.Throws<InvalidImageContentException>(() =>
{
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);
});
[Theory]
[WithFile(InvalidAll, PixelTypes.Rgba32)]
[WithFile(InvalidCompression, PixelTypes.Rgba32)] [WithFile(InvalidCompression, PixelTypes.Rgba32)]
[WithFile(InvalidRLE4, PixelTypes.Rgba32)] [WithFile(InvalidRLE4, PixelTypes.Rgba32)]
[WithFile(InvalidRLE8, PixelTypes.Rgba32)] [WithFile(InvalidRLE8, PixelTypes.Rgba32)]
public void InvalidTest(TestImageProvider<Rgba32> provider) public void InvalidThows_NotSupportedException(TestImageProvider<Rgba32> provider)
=> Assert.Throws<NotSupportedException>(() => => Assert.Throws<NotSupportedException>(() =>
{ {
using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance); using Image<Rgba32> image = provider.GetImage(IcoDecoder.Instance);

24
tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

@ -418,6 +418,21 @@ public partial class JpegDecoderTests
image.CompareToReferenceOutput(provider); image.CompareToReferenceOutput(provider);
} }
[Theory]
[WithFile(TestImages.Jpeg.ICC.Issue3064, PixelTypes.Rgba32)]
public void Decode_RGB_ICC_Jpeg_Issue3064<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
JpegDecoderOptions options = new()
{
GeneralOptions = new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert }
};
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance, options);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
}
// https://github.com/SixLabors/ImageSharp/issues/2948 // https://github.com/SixLabors/ImageSharp/issues/2948
[Theory] [Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)] [WithFile(TestImages.Jpeg.Issues.Issue2948, PixelTypes.Rgb24)]
@ -433,4 +448,13 @@ public partial class JpegDecoderTests
[InlineData(TestImages.Jpeg.Issues.Issue2948)] [InlineData(TestImages.Jpeg.Issues.Issue2948)]
public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath) public void Issue2948_No_SOS_Identify_Throws_InvalidImageContentException(string imagePath)
=> Assert.Throws<InvalidImageContentException>(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes)); => Assert.Throws<InvalidImageContentException>(() => _ = Image.Identify(TestFile.Create(imagePath).Bytes));
[Fact]
public void Issue_3071_Decode_TruncatedJpeg_Throws_InvalidImageContentException()
=> Assert.Throws<InvalidImageContentException>(() =>
{
// SOI marker (FF D8) + garbage bytes — only 11 bytes
byte[] data = [0xFF, 0xD8, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30];
using Image<Rgba32> image = Image.Load<Rgba32>(data);
});
} }

32
tests/ImageSharp.Tests/GraphicsOptionsTests.cs

@ -13,7 +13,7 @@ public class GraphicsOptionsTests
private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone(); private readonly GraphicsOptions cloneGraphicsOptions = new GraphicsOptions().DeepClone();
[Fact] [Fact]
public void CloneGraphicsOptionsIsNotNull() => Assert.True(this.cloneGraphicsOptions != null); public void CloneGraphicsOptionsIsNotNull() => Assert.NotNull(this.cloneGraphicsOptions);
[Fact] [Fact]
public void DefaultGraphicsOptionsAntialias() public void DefaultGraphicsOptionsAntialias()
@ -23,35 +23,35 @@ public class GraphicsOptionsTests
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsAntialiasSuppixelDepth() public void DefaultGraphicsOptionsAntialiasThreshold()
{ {
const int Expected = 16; const float expected = .5F;
Assert.Equal(Expected, this.newGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(expected, this.newGraphicsOptions.AntialiasThreshold);
Assert.Equal(Expected, this.cloneGraphicsOptions.AntialiasSubpixelDepth); Assert.Equal(expected, this.cloneGraphicsOptions.AntialiasThreshold);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsBlendPercentage() public void DefaultGraphicsOptionsBlendPercentage()
{ {
const float Expected = 1F; const float expected = 1F;
Assert.Equal(Expected, this.newGraphicsOptions.BlendPercentage); Assert.Equal(expected, this.newGraphicsOptions.BlendPercentage);
Assert.Equal(Expected, this.cloneGraphicsOptions.BlendPercentage); Assert.Equal(expected, this.cloneGraphicsOptions.BlendPercentage);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsColorBlendingMode() public void DefaultGraphicsOptionsColorBlendingMode()
{ {
const PixelColorBlendingMode Expected = PixelColorBlendingMode.Normal; const PixelColorBlendingMode expected = PixelColorBlendingMode.Normal;
Assert.Equal(Expected, this.newGraphicsOptions.ColorBlendingMode); Assert.Equal(expected, this.newGraphicsOptions.ColorBlendingMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.ColorBlendingMode); Assert.Equal(expected, this.cloneGraphicsOptions.ColorBlendingMode);
} }
[Fact] [Fact]
public void DefaultGraphicsOptionsAlphaCompositionMode() public void DefaultGraphicsOptionsAlphaCompositionMode()
{ {
const PixelAlphaCompositionMode Expected = PixelAlphaCompositionMode.SrcOver; const PixelAlphaCompositionMode expected = PixelAlphaCompositionMode.SrcOver;
Assert.Equal(Expected, this.newGraphicsOptions.AlphaCompositionMode); Assert.Equal(expected, this.newGraphicsOptions.AlphaCompositionMode);
Assert.Equal(Expected, this.cloneGraphicsOptions.AlphaCompositionMode); Assert.Equal(expected, this.cloneGraphicsOptions.AlphaCompositionMode);
} }
[Fact] [Fact]
@ -61,7 +61,7 @@ public class GraphicsOptionsTests
{ {
AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop, AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop,
Antialias = false, Antialias = false,
AntialiasSubpixelDepth = 23, AntialiasThreshold = .33F,
BlendPercentage = .25F, BlendPercentage = .25F,
ColorBlendingMode = PixelColorBlendingMode.HardLight, ColorBlendingMode = PixelColorBlendingMode.HardLight,
}; };
@ -79,7 +79,7 @@ public class GraphicsOptionsTests
actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop; actual.AlphaCompositionMode = PixelAlphaCompositionMode.DestAtop;
actual.Antialias = false; actual.Antialias = false;
actual.AntialiasSubpixelDepth = 23; actual.AntialiasThreshold = .67F;
actual.BlendPercentage = .25F; actual.BlendPercentage = .25F;
actual.ColorBlendingMode = PixelColorBlendingMode.HardLight; actual.ColorBlendingMode = PixelColorBlendingMode.HardLight;

1
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 SRgbGray = "Jpg/icc-profiles/sRGB_Gray.jpg";
public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg"; public const string Perceptual = "Jpg/icc-profiles/Perceptual.jpg";
public const string PerceptualcLUTOnly = "Jpg/icc-profiles/Perceptual-cLUT-only.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 public static class Progressive

12
tests/ImageSharp.Tests/TestUtilities/GraphicsOptionsComparer.cs

@ -6,13 +6,11 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities;
public class GraphicsOptionsComparer : IEqualityComparer<GraphicsOptions> public class GraphicsOptionsComparer : IEqualityComparer<GraphicsOptions>
{ {
public bool Equals(GraphicsOptions x, GraphicsOptions y) public bool Equals(GraphicsOptions x, GraphicsOptions y)
{ => x.AlphaCompositionMode == y.AlphaCompositionMode
return x.AlphaCompositionMode == y.AlphaCompositionMode && x.Antialias == y.Antialias
&& x.Antialias == y.Antialias && x.AntialiasThreshold == y.AntialiasThreshold
&& x.AntialiasSubpixelDepth == y.AntialiasSubpixelDepth && x.BlendPercentage == y.BlendPercentage
&& x.BlendPercentage == y.BlendPercentage && x.ColorBlendingMode == y.ColorBlendingMode;
&& x.ColorBlendingMode == y.ColorBlendingMode;
}
public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode(); public int GetHashCode(GraphicsOptions obj) => obj.GetHashCode();
} }

3
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

3
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
Loading…
Cancel
Save