Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Color conversion with ICC profiles #8

Open
wants to merge 70 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
16179b3
Copy original PR changes.
JimBobSquarePants Feb 27, 2021
eeb40f5
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Apr 23, 2021
8b96d37
Merge branch 'master' into icc-color-conversion
JimBobSquarePants May 22, 2021
aa2f1e9
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jul 7, 2021
3debb26
Merge branch 'master' into icc-color-conversion
brianpopow Jul 12, 2021
0812085
Fix warnings
brianpopow Jul 12, 2021
1094dc6
Add color conversion trait to be able to filter for those tests
brianpopow Jul 12, 2021
759f053
Fix failing MatrixCalculator test
brianpopow Jul 13, 2021
4cde4ef
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Nov 9, 2021
a973713
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Nov 11, 2021
33339d0
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jan 18, 2022
cdb495c
Merge branch 'master' into icc-color-conversion
JimBobSquarePants Jan 30, 2022
5f7acc1
Merge remote-tracking branch 'origin/main' into icc-color-conversion
brianpopow May 15, 2022
7e2bc20
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Nov 23, 2022
dc166a7
Fix build
JimBobSquarePants Nov 23, 2022
dcc8147
Cleanup icc tests
brianpopow Nov 24, 2022
73c8d8f
Fix Copyright notice
brianpopow Nov 24, 2022
d229fed
Fix icc namespaces
brianpopow Nov 24, 2022
0a08da0
Use file scoped namespaces
brianpopow Nov 27, 2022
daf366b
Cleanup and add conversion tests
JimBobSquarePants Nov 30, 2022
54856ff
Fix reader and out of range exception
JimBobSquarePants Dec 4, 2022
eee14c6
Remove invalid test.
JimBobSquarePants Dec 4, 2022
f60d4b8
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 14, 2022
8de137e
Cleanup code style
JimBobSquarePants Dec 15, 2022
fb8003c
Remove double clamping
JimBobSquarePants Dec 15, 2022
9f0f9cb
Optimize matrix read/write
JimBobSquarePants Dec 15, 2022
ece11eb
Create ColorProfileHandling.cs
JimBobSquarePants Dec 17, 2022
bd7257b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 17, 2022
9a21485
Nullable disable
JimBobSquarePants Dec 17, 2022
ba76964
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Dec 18, 2022
98d1758
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 2, 2023
66554cb
Add ability to convert ICC profile on decode
JimBobSquarePants Jan 4, 2023
e90f165
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 4, 2023
8c580a7
Handle nullability in decoder base
JimBobSquarePants Jan 5, 2023
a81dac9
Add reference files.
JimBobSquarePants Jan 9, 2023
902ed99
Add tolerance for Mac
JimBobSquarePants Jan 9, 2023
e3aa452
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 10, 2023
0dd68fe
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 15, 2023
6e3dc81
Update decoder bases following merge
JimBobSquarePants Jan 15, 2023
b7833a4
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jan 16, 2023
c3984aa
Add failing tests
JimBobSquarePants Jan 16, 2023
0fff06d
Port 1d, 2d, 3d, 4d and nd interpolation from reference implementation
brianpopow Jan 22, 2023
f1c05ee
Remove not used IccClut constructors
brianpopow Jan 22, 2023
3be31c3
Preserve alpha component
JimBobSquarePants Jan 22, 2023
6c2ee90
Fix CieLab docs
JimBobSquarePants Jan 22, 2023
20e9b7f
Fix out of bounds error
brianpopow Jan 22, 2023
d6fbc01
Change clut values from jagged array to flat array
brianpopow Jan 22, 2023
67ed4ce
Fix warnings
brianpopow Jan 22, 2023
b036cc3
Fix mistake reading the clut values
brianpopow Jan 22, 2023
52f88c8
Fix oob in n-dimension calculator.
JimBobSquarePants Jan 23, 2023
ed47678
Add Lab<=>Xyz conversion
JimBobSquarePants Jan 23, 2023
10bea86
Add ICC reader tests
brianpopow Jan 23, 2023
5b131ad
Add reference output for issue-129
brianpopow Feb 3, 2023
ed8091b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Feb 3, 2023
6225db3
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Feb 9, 2023
a65c599
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Mar 30, 2023
60f3d9d
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Sep 2, 2023
c940b86
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Sep 25, 2023
a567613
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Nov 1, 2023
3e06687
Update IccReader.cs
JimBobSquarePants Nov 1, 2023
c1ebbfe
Fix build
JimBobSquarePants Nov 1, 2023
d89d8c5
Update IccReader.cs
JimBobSquarePants Nov 1, 2023
63c89ca
Use scaled Vector4 conversion and optimize
JimBobSquarePants Nov 1, 2023
3389d7a
Add some debugging helpers to the converter
JimBobSquarePants Nov 9, 2023
7b0ff3b
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jun 11, 2024
29ed2b4
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jul 9, 2024
5f975e5
Update to latest main build
JimBobSquarePants Jul 9, 2024
79f5dfa
Update IccProfileConverterTests.cs
JimBobSquarePants Jul 9, 2024
b88b2a9
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Jul 10, 2024
bca4cad
Merge branch 'main' into icc-color-conversion
JimBobSquarePants Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
502 changes: 502 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/ColorTrcCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;

internal class ColorTrcCalculator : IVector4Calculator
{
private readonly TrcCalculator curveCalculator;
private Matrix4x4 matrix;
private readonly bool toPcs;

public ColorTrcCalculator(
IccXyzTagDataEntry redMatrixColumn,
IccXyzTagDataEntry greenMatrixColumn,
IccXyzTagDataEntry blueMatrixColumn,
IccTagDataEntry redTrc,
IccTagDataEntry greenTrc,
IccTagDataEntry blueTrc,
bool toPcs)
{
Comment on lines +16 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Missing null checks for all constructor parameters. Could throw NullReferenceException if any matrix columns or TRC entries are null.

this.toPcs = toPcs;
this.curveCalculator = new TrcCalculator(new IccTagDataEntry[] { redTrc, greenTrc, blueTrc }, !toPcs);

Vector3 mr = redMatrixColumn.Data[0];
Vector3 mg = greenMatrixColumn.Data[0];
Vector3 mb = blueMatrixColumn.Data[0];
Comment on lines +28 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Accessing Data[0] without checking array bounds could throw IndexOutOfRangeException if Data is empty.

this.matrix = new Matrix4x4(mr.X, mr.Y, mr.Z, 0, mg.X, mg.Y, mg.Z, 0, mb.X, mb.Y, mb.Z, 0, 0, 0, 0, 1);

if (!toPcs)
{
Matrix4x4.Invert(this.matrix, out this.matrix);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Matrix4x4.Invert() could fail for a singular matrix. Should check the return value and handle invalid matrices.

}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value)
{
if (this.toPcs)
{
value = this.curveCalculator.Calculate(value);
return Vector4.Transform(value, this.matrix);
}

value = Vector4.Transform(value, this.matrix);
return this.curveCalculator.Calculate(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;

internal partial class CurveCalculator
{
private enum CalculationType
{
Identity,
Gamma,
Lut,
}
}
47 changes: 47 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/CurveCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable

using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;

internal partial class CurveCalculator : ISingleCalculator
{
private readonly LutCalculator lutCalculator;
private readonly float gamma;
private readonly CalculationType type;

public CurveCalculator(IccCurveTagDataEntry entry, bool inverted)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: entry parameter should be validated with Guard.NotNull

{
if (entry.IsIdentityResponse)
{
this.type = CalculationType.Identity;
}
else if (entry.IsGamma)
{
this.gamma = entry.Gamma;
if (inverted)
{
this.gamma = 1f / this.gamma;
}

this.type = CalculationType.Gamma;
}
else
{
this.lutCalculator = new LutCalculator(entry.CurveData, inverted);
this.type = CalculationType.Lut;
}
}

public float Calculate(float value)
=> this.type switch
{
CalculationType.Identity => value,
CalculationType.Gamma => MathF.Pow(value, this.gamma), // TODO: This could be optimized using a LUT. See SrgbCompanding
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: gamma calculation could be optimized using a pre-computed LUT for common gamma values

CalculationType.Lut => this.lutCalculator.Calculate(value),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: check that lutCalculator is not null before using

_ => throw new InvalidOperationException("Invalid calculation type"),
};
}
19 changes: 19 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/GrayTrcCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;

internal class GrayTrcCalculator : IVector4Calculator
{
private readonly TrcCalculator calculator;

public GrayTrcCalculator(IccTagDataEntry grayTrc, bool toPcs)
=> this.calculator = new TrcCalculator(new IccTagDataEntry[] { grayTrc }, !toPcs);
Comment on lines +14 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Consider adding Guard.NotNull check for grayTrc parameter to prevent null reference exceptions


[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Vector4 Calculate(Vector4 value) => this.calculator.Calculate(value);
}
17 changes: 17 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/ISingleCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;

/// <summary>
/// Represents an ICC calculator with a single floating point value and result
/// </summary>
internal interface ISingleCalculator
{
/// <summary>
/// Calculates a result from the given value
/// </summary>
/// <param name="value">The input value</param>
/// <returns>The calculated result</returns>
float Calculate(float value);
}
19 changes: 19 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/IVector4Calculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

using System.Numerics;

namespace SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;

/// <summary>
/// Represents an ICC calculator with <see cref="Vector4"/> values and results
/// </summary>
internal interface IVector4Calculator
{
/// <summary>
/// Calculates a result from the given values
/// </summary>
/// <param name="value">The input values</param>
/// <returns>The calculated result</returns>
Vector4 Calculate(Vector4 value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;

internal partial class LutABCalculator
{
private enum CalculationType
{
AtoB = 1 << 3,
BtoA = 1 << 4,

SingleCurve = 1,
CurveMatrix = 2,
CurveClut = 3,
Full = 4,
Comment on lines +13 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: sequential values 1-4 suggest these are not meant to be flags but rather enum values - this could cause issues when combining with AtoB/BtoA flags

}
}
135 changes: 135 additions & 0 deletions src/ImageSharp/ColorProfiles/Icc/Calculators/LutABCalculator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
#nullable disable

using System.Numerics;
using SixLabors.ImageSharp.ColorProfiles.Icc.Calculators;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;

namespace SixLabors.ImageSharp.ColorSpaces.Conversion.Icc;

internal partial class LutABCalculator : IVector4Calculator
{
private CalculationType type;
private TrcCalculator curveACalculator;
private TrcCalculator curveBCalculator;
private TrcCalculator curveMCalculator;
private MatrixCalculator matrixCalculator;
private ClutCalculator clutCalculator;

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;
}

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;
}

public Vector4 Calculate(Vector4 value)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Consider adding XML documentation for the Calculate method to describe expected input ranges and behavior

{
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);

default:
throw new InvalidOperationException("Invalid calculation type");
}
}

private void Init(IccTagDataEntry[] curveA, IccTagDataEntry[] curveB, IccTagDataEntry[] curveM, Vector3? matrix3x1, Matrix4x4? matrix3x3, IccClut clut)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The Init method could potentially be called multiple times. Consider making it throw if already initialized

{
bool hasACurve = curveA != null;
bool hasBCurve = curveB != null;
bool hasMCurve = curveM != null;
bool hasMatrix = matrix3x1 != null && matrix3x3 != null;
bool hasClut = clut != null;

if (hasBCurve && hasMatrix && hasMCurve && hasClut && hasACurve)
{
this.type = CalculationType.Full;
}
Comment on lines +89 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: The order of checks may allow invalid combinations to pass. Consider validating that all required components are present for each type before setting

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Calculators are not checked for null before use in Calculate method, which could cause NullReferenceException

{
this.curveACalculator = new TrcCalculator(curveA, false);
}

if (hasBCurve)
{
this.curveBCalculator = new TrcCalculator(curveB, false);
}

if (hasMCurve)
{
this.curveMCalculator = new TrcCalculator(curveM, false);
}

if (hasMatrix)
{
this.matrixCalculator = new MatrixCalculator(matrix3x3.Value, matrix3x1.Value);
}

if (hasClut)
{
this.clutCalculator = new ClutCalculator(clut);
}
}
}
Loading
Loading