Skip to content

Commit 4bb4baf

Browse files
Merge pull request #3070 from SixLabors/js/fix-3064
Add sparse ICC v4 mAB/mBA LUT support for ICC conversion
2 parents ec79a2f + 4fc4d66 commit 4bb4baf

11 files changed

Lines changed: 292 additions & 246 deletions

File tree

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ namespace SixLabors.ImageSharp.ColorProfiles.Conversion.Icc;
55

66
internal partial class LutABCalculator
77
{
8+
/// <summary>
9+
/// Identifies the transform direction for the configured LUT calculator.
10+
/// </summary>
811
private enum CalculationType
912
{
10-
AtoB = 1 << 3,
11-
BtoA = 1 << 4,
13+
/// <summary>
14+
/// Converts from device space to PCS using ICC <c>mAB</c> stage order.
15+
/// </summary>
16+
AtoB,
1217

13-
SingleCurve = 1,
14-
CurveMatrix = 2,
15-
CurveClut = 3,
16-
Full = 4,
18+
/// <summary>
19+
/// Converts from PCS to device space using ICC <c>mBA</c> stage order.
20+
/// </summary>
21+
BtoA,
1722
}
1823
}

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

Lines changed: 82 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -17,67 +17,106 @@ internal partial class LutABCalculator : IVector4Calculator
1717
private MatrixCalculator matrixCalculator;
1818
private ClutCalculator clutCalculator;
1919

20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mAB</c> transform.
22+
/// </summary>
23+
/// <param name="entry">The parsed A-to-B LUT entry.</param>
2024
public LutABCalculator(IccLutAToBTagDataEntry entry)
2125
{
2226
Guard.NotNull(entry, nameof(entry));
2327
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
24-
this.type |= CalculationType.AtoB;
28+
this.type = CalculationType.AtoB;
2529
}
2630

31+
/// <summary>
32+
/// Initializes a new instance of the <see cref="LutABCalculator"/> class for an ICC <c>mBA</c> transform.
33+
/// </summary>
34+
/// <param name="entry">The parsed B-to-A LUT entry.</param>
2735
public LutABCalculator(IccLutBToATagDataEntry entry)
2836
{
2937
Guard.NotNull(entry, nameof(entry));
3038
this.Init(entry.CurveA, entry.CurveB, entry.CurveM, entry.Matrix3x1, entry.Matrix3x3, entry.ClutValues);
31-
this.type |= CalculationType.BtoA;
39+
this.type = CalculationType.BtoA;
3240
}
3341

42+
/// <summary>
43+
/// Calculates the transformed value by applying the configured ICC LUT stages in specification order.
44+
/// </summary>
45+
/// <param name="value">The input value.</param>
46+
/// <returns>The transformed value.</returns>
3447
public Vector4 Calculate(Vector4 value)
3548
{
3649
switch (this.type)
3750
{
38-
case CalculationType.Full | CalculationType.AtoB:
39-
value = this.curveACalculator.Calculate(value);
40-
value = this.clutCalculator.Calculate(value);
41-
value = this.curveMCalculator.Calculate(value);
42-
value = this.matrixCalculator.Calculate(value);
43-
return this.curveBCalculator.Calculate(value);
44-
45-
case CalculationType.Full | CalculationType.BtoA:
46-
value = this.curveBCalculator.Calculate(value);
47-
value = this.matrixCalculator.Calculate(value);
48-
value = this.curveMCalculator.Calculate(value);
49-
value = this.clutCalculator.Calculate(value);
50-
return this.curveACalculator.Calculate(value);
51-
52-
case CalculationType.CurveClut | CalculationType.AtoB:
53-
value = this.curveACalculator.Calculate(value);
54-
value = this.clutCalculator.Calculate(value);
55-
return this.curveBCalculator.Calculate(value);
56-
57-
case CalculationType.CurveClut | CalculationType.BtoA:
58-
value = this.curveBCalculator.Calculate(value);
59-
value = this.clutCalculator.Calculate(value);
60-
return this.curveACalculator.Calculate(value);
61-
62-
case CalculationType.CurveMatrix | CalculationType.AtoB:
63-
value = this.curveMCalculator.Calculate(value);
64-
value = this.matrixCalculator.Calculate(value);
65-
return this.curveBCalculator.Calculate(value);
66-
67-
case CalculationType.CurveMatrix | CalculationType.BtoA:
68-
value = this.curveBCalculator.Calculate(value);
69-
value = this.matrixCalculator.Calculate(value);
70-
return this.curveMCalculator.Calculate(value);
71-
72-
case CalculationType.SingleCurve | CalculationType.AtoB:
73-
case CalculationType.SingleCurve | CalculationType.BtoA:
74-
return this.curveBCalculator.Calculate(value);
51+
case CalculationType.AtoB:
52+
// ICC mAB order: A, CLUT, M, Matrix, B.
53+
if (this.curveACalculator != null)
54+
{
55+
value = this.curveACalculator.Calculate(value);
56+
}
57+
58+
if (this.clutCalculator != null)
59+
{
60+
value = this.clutCalculator.Calculate(value);
61+
}
62+
63+
if (this.curveMCalculator != null)
64+
{
65+
value = this.curveMCalculator.Calculate(value);
66+
}
67+
68+
if (this.matrixCalculator != null)
69+
{
70+
value = this.matrixCalculator.Calculate(value);
71+
}
72+
73+
if (this.curveBCalculator != null)
74+
{
75+
value = this.curveBCalculator.Calculate(value);
76+
}
77+
78+
return value;
79+
80+
case CalculationType.BtoA:
81+
// ICC mBA order: B, Matrix, M, CLUT, A.
82+
if (this.curveBCalculator != null)
83+
{
84+
value = this.curveBCalculator.Calculate(value);
85+
}
86+
87+
if (this.matrixCalculator != null)
88+
{
89+
value = this.matrixCalculator.Calculate(value);
90+
}
91+
92+
if (this.curveMCalculator != null)
93+
{
94+
value = this.curveMCalculator.Calculate(value);
95+
}
96+
97+
if (this.clutCalculator != null)
98+
{
99+
value = this.clutCalculator.Calculate(value);
100+
}
101+
102+
if (this.curveACalculator != null)
103+
{
104+
value = this.curveACalculator.Calculate(value);
105+
}
106+
107+
return value;
75108

76109
default:
77110
throw new InvalidOperationException("Invalid calculation type");
78111
}
79112
}
80113

114+
/// <summary>
115+
/// Creates calculators for the processing stages present in the LUT entry.
116+
/// </summary>
117+
/// <remarks>
118+
/// The tag entry classes already validate channel continuity, so this method only materializes the available stages.
119+
/// </remarks>
81120
private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
82121
{
83122
bool hasACurve = curveA != null;
@@ -86,26 +125,10 @@ private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagData
86125
bool hasMatrix = matrix3x1 != null && matrix3x3 != null;
87126
bool hasClut = clut != null;
88127

89-
if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve)
90-
{
91-
this.type = CalculationType.Full;
92-
}
93-
else if (hasBCurve && hasClut && hasACurve)
94-
{
95-
this.type = CalculationType.CurveClut;
96-
}
97-
else if (hasBCurve && hasMatrix && hasMCurve)
98-
{
99-
this.type = CalculationType.CurveMatrix;
100-
}
101-
else if (hasBCurve)
102-
{
103-
this.type = CalculationType.SingleCurve;
104-
}
105-
else
106-
{
107-
throw new InvalidIccProfileException("AToB or BToA tag has an invalid configuration");
108-
}
128+
Guard.IsTrue(
129+
hasACurve || hasBCurve || hasMCurve || hasMatrix || hasClut,
130+
"entry",
131+
"AToB or BToA tag must contain at least one processing element");
109132

110133
if (hasACurve)
111134
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ private static IVector4Calculator InitA(IccProfile profile, IccProfileTag tag)
6060
IccLut16TagDataEntry lut16 => new LutEntryCalculator(lut16),
6161
IccLutAToBTagDataEntry lutAtoB => new LutABCalculator(lutAtoB),
6262
IccLutBToATagDataEntry lutBtoA => new LutABCalculator(lutBtoA),
63-
_ => throw new InvalidIccProfileException("Invalid entry."),
63+
_ => throw new InvalidIccProfileException($"Invalid entry {tag}."),
6464
};
6565

6666
private static IVector4Calculator InitD(IccProfile profile, IccProfileTag tag)

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

Lines changed: 37 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -20,82 +20,46 @@ internal sealed partial class IccDataReader
2020
public IccTagDataEntry ReadTagDataEntry(IccTagTableEntry info)
2121
{
2222
this.currentIndex = (int)info.Offset;
23-
switch (this.ReadTagDataEntryHeader())
24-
{
25-
case IccTypeSignature.Chromaticity:
26-
return this.ReadChromaticityTagDataEntry();
27-
case IccTypeSignature.ColorantOrder:
28-
return this.ReadColorantOrderTagDataEntry();
29-
case IccTypeSignature.ColorantTable:
30-
return this.ReadColorantTableTagDataEntry();
31-
case IccTypeSignature.Curve:
32-
return this.ReadCurveTagDataEntry();
33-
case IccTypeSignature.Data:
34-
return this.ReadDataTagDataEntry(info.DataSize);
35-
case IccTypeSignature.DateTime:
36-
return this.ReadDateTimeTagDataEntry();
37-
case IccTypeSignature.Lut16:
38-
return this.ReadLut16TagDataEntry();
39-
case IccTypeSignature.Lut8:
40-
return this.ReadLut8TagDataEntry();
41-
case IccTypeSignature.LutAToB:
42-
return this.ReadLutAtoBTagDataEntry();
43-
case IccTypeSignature.LutBToA:
44-
return this.ReadLutBtoATagDataEntry();
45-
case IccTypeSignature.Measurement:
46-
return this.ReadMeasurementTagDataEntry();
47-
case IccTypeSignature.MultiLocalizedUnicode:
48-
return this.ReadMultiLocalizedUnicodeTagDataEntry();
49-
case IccTypeSignature.MultiProcessElements:
50-
return this.ReadMultiProcessElementsTagDataEntry();
51-
case IccTypeSignature.NamedColor2:
52-
return this.ReadNamedColor2TagDataEntry();
53-
case IccTypeSignature.ParametricCurve:
54-
return this.ReadParametricCurveTagDataEntry();
55-
case IccTypeSignature.ProfileSequenceDesc:
56-
return this.ReadProfileSequenceDescTagDataEntry();
57-
case IccTypeSignature.ProfileSequenceIdentifier:
58-
return this.ReadProfileSequenceIdentifierTagDataEntry();
59-
case IccTypeSignature.ResponseCurveSet16:
60-
return this.ReadResponseCurveSet16TagDataEntry();
61-
case IccTypeSignature.S15Fixed16Array:
62-
return this.ReadFix16ArrayTagDataEntry(info.DataSize);
63-
case IccTypeSignature.Signature:
64-
return this.ReadSignatureTagDataEntry();
65-
case IccTypeSignature.Text:
66-
return this.ReadTextTagDataEntry(info.DataSize);
67-
case IccTypeSignature.U16Fixed16Array:
68-
return this.ReadUFix16ArrayTagDataEntry(info.DataSize);
69-
case IccTypeSignature.UInt16Array:
70-
return this.ReadUInt16ArrayTagDataEntry(info.DataSize);
71-
case IccTypeSignature.UInt32Array:
72-
return this.ReadUInt32ArrayTagDataEntry(info.DataSize);
73-
case IccTypeSignature.UInt64Array:
74-
return this.ReadUInt64ArrayTagDataEntry(info.DataSize);
75-
case IccTypeSignature.UInt8Array:
76-
return this.ReadUInt8ArrayTagDataEntry(info.DataSize);
77-
case IccTypeSignature.ViewingConditions:
78-
return this.ReadViewingConditionsTagDataEntry();
79-
case IccTypeSignature.Xyz:
80-
return this.ReadXyzTagDataEntry(info.DataSize);
23+
return this.ReadTagDataEntryHeader() switch
24+
{
25+
IccTypeSignature.Chromaticity => this.ReadChromaticityTagDataEntry(),
26+
IccTypeSignature.ColorantOrder => this.ReadColorantOrderTagDataEntry(),
27+
IccTypeSignature.ColorantTable => this.ReadColorantTableTagDataEntry(),
28+
IccTypeSignature.Curve => this.ReadCurveTagDataEntry(),
29+
IccTypeSignature.Data => this.ReadDataTagDataEntry(info.DataSize),
30+
IccTypeSignature.DateTime => this.ReadDateTimeTagDataEntry(),
31+
IccTypeSignature.Lut16 => this.ReadLut16TagDataEntry(),
32+
IccTypeSignature.Lut8 => this.ReadLut8TagDataEntry(),
33+
IccTypeSignature.LutAToB => this.ReadLutAtoBTagDataEntry(),
34+
IccTypeSignature.LutBToA => this.ReadLutBtoATagDataEntry(),
35+
IccTypeSignature.Measurement => this.ReadMeasurementTagDataEntry(),
36+
IccTypeSignature.MultiLocalizedUnicode => this.ReadMultiLocalizedUnicodeTagDataEntry(),
37+
IccTypeSignature.MultiProcessElements => this.ReadMultiProcessElementsTagDataEntry(),
38+
IccTypeSignature.NamedColor2 => this.ReadNamedColor2TagDataEntry(),
39+
IccTypeSignature.ParametricCurve => this.ReadParametricCurveTagDataEntry(),
40+
IccTypeSignature.ProfileSequenceDesc => this.ReadProfileSequenceDescTagDataEntry(),
41+
IccTypeSignature.ProfileSequenceIdentifier => this.ReadProfileSequenceIdentifierTagDataEntry(),
42+
IccTypeSignature.ResponseCurveSet16 => this.ReadResponseCurveSet16TagDataEntry(),
43+
IccTypeSignature.S15Fixed16Array => this.ReadFix16ArrayTagDataEntry(info.DataSize),
44+
IccTypeSignature.Signature => this.ReadSignatureTagDataEntry(),
45+
IccTypeSignature.Text => this.ReadTextTagDataEntry(info.DataSize),
46+
IccTypeSignature.U16Fixed16Array => this.ReadUFix16ArrayTagDataEntry(info.DataSize),
47+
IccTypeSignature.UInt16Array => this.ReadUInt16ArrayTagDataEntry(info.DataSize),
48+
IccTypeSignature.UInt32Array => this.ReadUInt32ArrayTagDataEntry(info.DataSize),
49+
IccTypeSignature.UInt64Array => this.ReadUInt64ArrayTagDataEntry(info.DataSize),
50+
IccTypeSignature.UInt8Array => this.ReadUInt8ArrayTagDataEntry(info.DataSize),
51+
IccTypeSignature.ViewingConditions => this.ReadViewingConditionsTagDataEntry(),
52+
IccTypeSignature.Xyz => this.ReadXyzTagDataEntry(info.DataSize),
8153

8254
// V2 Types:
83-
case IccTypeSignature.TextDescription:
84-
return this.ReadTextDescriptionTagDataEntry();
85-
case IccTypeSignature.CrdInfo:
86-
return this.ReadCrdInfoTagDataEntry();
87-
case IccTypeSignature.Screening:
88-
return this.ReadScreeningTagDataEntry();
89-
case IccTypeSignature.UcrBg:
90-
return this.ReadUcrBgTagDataEntry(info.DataSize);
55+
IccTypeSignature.TextDescription => this.ReadTextDescriptionTagDataEntry(),
56+
IccTypeSignature.CrdInfo => this.ReadCrdInfoTagDataEntry(),
57+
IccTypeSignature.Screening => this.ReadScreeningTagDataEntry(),
58+
IccTypeSignature.UcrBg => this.ReadUcrBgTagDataEntry(info.DataSize),
9159

9260
// Unsupported or unknown
93-
case IccTypeSignature.DeviceSettings:
94-
case IccTypeSignature.NamedColor:
95-
case IccTypeSignature.Unknown:
96-
default:
97-
return this.ReadUnknownTagDataEntry(info.DataSize);
98-
}
61+
_ => this.ReadUnknownTagDataEntry(info.DataSize),
62+
};
9963
}
10064

10165
/// <summary>
@@ -477,7 +441,7 @@ public IccMultiLocalizedUnicodeTagDataEntry ReadMultiLocalizedUnicodeTagDataEntr
477441

478442
return new IccMultiLocalizedUnicodeTagDataEntry(text);
479443

480-
CultureInfo ReadCulture(string language, string country)
444+
static CultureInfo ReadCulture(string language, string country)
481445
{
482446
if (string.IsNullOrWhiteSpace(language))
483447
{

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ public static IccProfileId CalculateHash(byte[] data)
110110
// need to copy some values because they need to be zero for the hashing
111111
Span<byte> temp = stackalloc byte[24];
112112
data.AsSpan(profileFlagPos, 4).CopyTo(temp);
113-
data.AsSpan(renderingIntentPos, 4).CopyTo(temp.Slice(4));
114-
data.AsSpan(profileIdPos, 16).CopyTo(temp.Slice(8));
113+
data.AsSpan(renderingIntentPos, 4).CopyTo(temp[4..]);
114+
data.AsSpan(profileIdPos, 16).CopyTo(temp[8..]);
115115

116116
try
117117
{
@@ -131,7 +131,7 @@ public static IccProfileId CalculateHash(byte[] data)
131131
}
132132
finally
133133
{
134-
temp.Slice(0, 4).CopyTo(data.AsSpan(profileFlagPos));
134+
temp[..4].CopyTo(data.AsSpan(profileFlagPos));
135135
temp.Slice(4, 4).CopyTo(data.AsSpan(renderingIntentPos));
136136
temp.Slice(8, 16).CopyTo(data.AsSpan(profileIdPos));
137137
}

0 commit comments

Comments
 (0)