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

Conversation

lizard-boy
Copy link

@lizard-boy lizard-boy commented Nov 1, 2024

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

Note: This is a replacement for the original PR SixLabors#273 from @JBildstein that was automatically closed by our Git LFS history rewrite. Individual commits have unfortunately been lost in the process. Help is very much needed to complete the work.

As the title says, this adds methods for converting colors with an ICC profile.

Architecturally, the idea is that the profile is checked once for available and appropriate conversion methods and a then a delegate is stored that only takes the color values to convert and returns the calculated values. The possible performance penalty for using a delegate is far smaller than searching through the profile for every conversion. I'm open for other suggestions though.

There are classes to convert from the profile connection space (=PCS, can be XYZ or Lab) to the data space (RGB, CMYK, etc.) and vice versa. There are also classes to convert from PCS to PCS and Data to Data but they are only used for special profiles and are not important for us now but I still added them for completeness sake.

A challenge here is writing tests for this because of the complexity of the calculations and the big amount of different possible conversion paths. This is a rough list of the paths that exist:

"A to B" and "B to A" tags
IccLut8TagDataEntry
Input IccLut[], Clut, Output IccLut[]
Matrix(3x3), Input IccLut[], IccClut, Output IccLut[]
IccLut16TagDataEntry
Input IccLut[], IccClut, Output IccLut[]
Matrix(3x3), Input IccLut[], IccClut, Output IccLut[]
IccLutAToBTagDataEntry/IccLutBToATagDataEntry (Curve types can either be IccCurveTagDataEntry or IccParametricCurveTagDataEntry (which has several curve subtypes))
CurveA[], Clut, CurveM[], Matrix(3x1), Matrix(3x3), CurveB[]
CurveA[], Clut, CurveB[]
CurveM[], Matrix(3x1), Matrix(3x3), CurveB[]
CurveB[]
"D to B" tags
IccMultiProcessElementsTagDataEntry that contains an array of any of those types in any order:
IccCurveSetProcessElement
IccOneDimensionalCurve[] where each curve can have several curve subtypes
IccMatrixProcessElement
Matrix(Nr. of input Channels by Nr. of output Channels), Matrix(Nr. of output channels by 1)
IccClutProcessElement
IccClut
Color Trc
Matrix(3x3), one curve for R, G and B each (Curve types can either be IccCurveTagDataEntry or IccParametricCurveTagDataEntry (which has several curve subtypes))
Gray Trc
Curve (Curve type can either be IccCurveTagDataEntry or IccParametricCurveTagDataEntry (which has several curve subtypes))
The three main approaches in that list are

A to B/B to A: using a combination of lookup tables, matrices and curves
D to B: using a chain of multi process elements (curves, matrices or lookup)
Trc: using curves (and matrices for color but not for gray)
The most used approaches are Color Trc for RGB profiles and LutAToB/LutBToA for CMYK profiles.

Todo list:

  • Integrate with the rest of the project
  • Write tests that cover all conversion paths
  • Review architecture
  • Improve speed and accuracy of the calculations

Greptile Summary

This PR introduces comprehensive ICC color profile conversion functionality to ImageSharp, with a focus on lookup tables, tone reproduction curves, and matrix transformations.

  • Added ClutCalculator in /src/ImageSharp/ColorProfiles/Icc/Calculators/ClutCalculator.cs for N-dimensional color lookup table interpolation
  • Added IccProfileConverter in /src/ImageSharp/ColorProfiles/Icc/IccProfileConverter.cs for core color space conversion between ICC profiles
  • Added ColorProfileHandling enum in /src/ImageSharp/Formats/ColorProfileHandling.cs to control ICC profile preservation during decoding
  • Added various calculator implementations (TRC, parametric curves, matrices) in /src/ImageSharp/ColorProfiles/Icc/Calculators/ for different conversion paths
  • Modified decoder classes to support ICC profile transformation during image loading

@lizard-boy lizard-boy added enhancement New feature or request help wanted Extra attention is needed formats:jpeg metadata:icc labels Nov 1, 2024
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

30 file(s) reviewed, 62 comment(s)
Edit PR Review Bot Settings | Greptile

Comment on lines +43 to +44
Guard.MustBeGreaterThan(clut.InputChannelCount, 1, nameof(clut.InputChannelCount));
Guard.MustBeGreaterThan(clut.OutputChannelCount, 1, nameof(clut.OutputChannelCount));
Copy link

Choose a reason for hiding this comment

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

logic: Input/output channel count validation requires > 1, but ICC spec allows single-channel CLUTs. Should be >= 1 instead.

this.nPower[count] = (uint)(1 << (this.inputCount - 1 - count));
}

uint[] nPower = [0, 1];
Copy link

Choose a reason for hiding this comment

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

style: nPower array initialized with collection expression syntax which requires C# 12 - may cause compatibility issues

}

Span<float> p = this.lut.AsSpan(index);
float[] temp = new float[2];
Copy link

Choose a reason for hiding this comment

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

style: temp array allocation in hot path could be moved to class field to avoid repeated allocations

Comment on lines +16 to +24
public ColorTrcCalculator(
IccXyzTagDataEntry redMatrixColumn,
IccXyzTagDataEntry greenMatrixColumn,
IccXyzTagDataEntry blueMatrixColumn,
IccTagDataEntry redTrc,
IccTagDataEntry greenTrc,
IccTagDataEntry blueTrc,
bool toPcs)
{
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.

Comment on lines +28 to +30
Vector3 mr = redMatrixColumn.Data[0];
Vector3 mg = greenMatrixColumn.Data[0];
Vector3 mb = blueMatrixColumn.Data[0];
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.

Comment on lines +71 to 72
TransformColorProfile(options.GeneralOptions, image);
this.SetDecoderFormat(options.GeneralOptions.Configuration, image);
Copy link

Choose a reason for hiding this comment

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

logic: Async methods should use ConfigureAwait(false) consistently - TransformColorProfile may need to be awaitable if it performs async operations

/// <param name="inChannelCount">Input channel count.</param>
/// <param name="outChannelCount">Output channel count.</param>
/// <param name="gridPointCount">Grid point count for each CLUT channel.</param>
/// <returns>The read CLUT16.</returns>
public IccClut ReadClut16(int inChannelCount, int outChannelCount, byte[] gridPointCount)
{
int start = this.currentIndex;
Copy link

Choose a reason for hiding this comment

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

style: start variable is used for currentIndex calculation but could be removed by using offset-based calculation like in ReadClut8

/// <param name="inChCount">Input channel count.</param>
/// <param name="outChCount">Output channel count.</param>
/// <param name="gridPointCount">Grid point count for each CLUT channel.</param>
/// <returns>The read CLUTf32.</returns>
public IccClut ReadClutF32(int inChCount, int outChCount, byte[] gridPointCount)
{
int start = this.currentIndex;
Copy link

Choose a reason for hiding this comment

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

style: start variable could be removed like in ReadClut8 method for consistency

@@ -17,16 +17,23 @@ internal sealed partial class IccDataReader
/// <returns>The read matrix</returns>
public float[,] ReadMatrix(int xCount, int yCount, bool isSingle)
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 input validation for xCount and yCount to prevent negative dimensions

@@ -44,14 +51,17 @@ internal sealed partial class IccDataReader
/// <returns>The read matrix</returns>
public float[] ReadMatrix(int yCount, bool isSingle)
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 input validation for yCount to prevent negative dimension

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request formats:jpeg help wanted Extra attention is needed metadata:icc
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants