| // Copyright 2023 Google LLC |
| // SPDX-License-Identifier: BSD-2-Clause |
| |
| #include "avif/internal.h" |
| |
| #include <math.h> |
| |
| static const double epsilon = 1e-12; |
| |
| static avifBool avifXyToXYZ(const float xy[2], double XYZ[3]) |
| { |
| if (fabsf(xy[1]) < epsilon) { |
| return AVIF_FALSE; |
| } |
| |
| const double factor = 1.0 / xy[1]; |
| XYZ[0] = xy[0] * factor; |
| XYZ[1] = 1; |
| XYZ[2] = (1 - xy[0] - xy[1]) * factor; |
| |
| return AVIF_TRUE; |
| } |
| |
| // Computes I = M^-1. Returns false if M seems to be singular. |
| static avifBool avifMatInv(double M[3][3], double I[3][3]) |
| { |
| double det = M[0][0] * (M[1][1] * M[2][2] - M[2][1] * M[1][2]) - M[0][1] * (M[1][0] * M[2][2] - M[1][2] * M[2][0]) + |
| M[0][2] * (M[1][0] * M[2][1] - M[1][1] * M[2][0]); |
| if (fabs(det) < epsilon) { |
| return AVIF_FALSE; |
| } |
| det = 1.0 / det; |
| |
| I[0][0] = (M[1][1] * M[2][2] - M[2][1] * M[1][2]) * det; |
| I[0][1] = (M[0][2] * M[2][1] - M[0][1] * M[2][2]) * det; |
| I[0][2] = (M[0][1] * M[1][2] - M[0][2] * M[1][1]) * det; |
| I[1][0] = (M[1][2] * M[2][0] - M[1][0] * M[2][2]) * det; |
| I[1][1] = (M[0][0] * M[2][2] - M[0][2] * M[2][0]) * det; |
| I[1][2] = (M[1][0] * M[0][2] - M[0][0] * M[1][2]) * det; |
| I[2][0] = (M[1][0] * M[2][1] - M[2][0] * M[1][1]) * det; |
| I[2][1] = (M[2][0] * M[0][1] - M[0][0] * M[2][1]) * det; |
| I[2][2] = (M[0][0] * M[1][1] - M[1][0] * M[0][1]) * det; |
| |
| return AVIF_TRUE; |
| } |
| |
| // Computes C = A*B |
| static void avifMatMul(double A[3][3], double B[3][3], double C[3][3]) |
| { |
| C[0][0] = A[0][0] * B[0][0] + A[0][1] * B[1][0] + A[0][2] * B[2][0]; |
| C[0][1] = A[0][0] * B[0][1] + A[0][1] * B[1][1] + A[0][2] * B[2][1]; |
| C[0][2] = A[0][0] * B[0][2] + A[0][1] * B[1][2] + A[0][2] * B[2][2]; |
| C[1][0] = A[1][0] * B[0][0] + A[1][1] * B[1][0] + A[1][2] * B[2][0]; |
| C[1][1] = A[1][0] * B[0][1] + A[1][1] * B[1][1] + A[1][2] * B[2][1]; |
| C[1][2] = A[1][0] * B[0][2] + A[1][1] * B[1][2] + A[1][2] * B[2][2]; |
| C[2][0] = A[2][0] * B[0][0] + A[2][1] * B[1][0] + A[2][2] * B[2][0]; |
| C[2][1] = A[2][0] * B[0][1] + A[2][1] * B[1][1] + A[2][2] * B[2][1]; |
| C[2][2] = A[2][0] * B[0][2] + A[2][1] * B[1][2] + A[2][2] * B[2][2]; |
| } |
| |
| // Set M to have values of d on the leading diagonal, and zero elsewhere. |
| static void avifMatDiag(const double d[3], double M[3][3]) |
| { |
| M[0][0] = d[0]; |
| M[0][1] = 0; |
| M[0][2] = 0; |
| M[1][0] = 0; |
| M[1][1] = d[1]; |
| M[1][2] = 0; |
| M[2][0] = 0; |
| M[2][1] = 0; |
| M[2][2] = d[2]; |
| } |
| |
| // Computes y = M.x |
| static void avifVecMul(double M[3][3], const double x[3], double y[3]) |
| { |
| y[0] = M[0][0] * x[0] + M[0][1] * x[1] + M[0][2] * x[2]; |
| y[1] = M[1][0] * x[0] + M[1][1] * x[1] + M[1][2] * x[2]; |
| y[2] = M[2][0] * x[0] + M[2][1] * x[1] + M[2][2] * x[2]; |
| } |
| |
| // Bradford chromatic adaptation matrix |
| // from https://www.researchgate.net/publication/253799640_A_uniform_colour_space_based_upon_CIECAM97s |
| static double avifBradford[3][3] = { |
| { 0.8951, 0.2664, -0.1614 }, |
| { -0.7502, 1.7135, 0.0367 }, |
| { 0.0389, -0.0685, 1.0296 }, |
| }; |
| |
| // LMS values for D50 whitepoint |
| static const double avifLmsD50[3] = { 0.996284, 1.02043, 0.818644 }; |
| |
| avifBool avifColorPrimariesComputeRGBToXYZD50Matrix(avifColorPrimaries colorPrimaries, double coeffs[3][3]) |
| { |
| float primaries[8]; |
| avifColorPrimariesGetValues(colorPrimaries, primaries); |
| |
| double whitePointXYZ[3]; |
| AVIF_CHECK(avifXyToXYZ(&primaries[6], whitePointXYZ)); |
| |
| double rgbPrimaries[3][3] = { |
| { primaries[0], primaries[2], primaries[4] }, |
| { primaries[1], primaries[3], primaries[5] }, |
| { 1.0 - primaries[0] - primaries[1], 1.0 - primaries[2] - primaries[3], 1.0 - primaries[4] - primaries[5] } |
| }; |
| |
| double rgbPrimariesInv[3][3]; |
| AVIF_CHECK(avifMatInv(rgbPrimaries, rgbPrimariesInv)); |
| |
| double rgbCoefficients[3]; |
| avifVecMul(rgbPrimariesInv, whitePointXYZ, rgbCoefficients); |
| |
| double rgbCoefficientsMat[3][3]; |
| avifMatDiag(rgbCoefficients, rgbCoefficientsMat); |
| |
| double rgbXYZ[3][3]; |
| avifMatMul(rgbPrimaries, rgbCoefficientsMat, rgbXYZ); |
| |
| // ICC stores primaries XYZ under PCS. |
| // Adapt using linear bradford transform |
| // from https://onlinelibrary.wiley.com/doi/pdf/10.1002/9781119021780.app3 |
| double lms[3]; |
| avifVecMul(avifBradford, whitePointXYZ, lms); |
| for (int i = 0; i < 3; ++i) { |
| if (fabs(lms[i]) < epsilon) { |
| return AVIF_FALSE; |
| } |
| lms[i] = avifLmsD50[i] / lms[i]; |
| } |
| |
| double adaptation[3][3]; |
| avifMatDiag(lms, adaptation); |
| |
| double tmp[3][3]; |
| avifMatMul(adaptation, avifBradford, tmp); |
| |
| double bradfordInv[3][3]; |
| if (!avifMatInv(avifBradford, bradfordInv)) { |
| return AVIF_FALSE; |
| } |
| avifMatMul(bradfordInv, tmp, adaptation); |
| |
| avifMatMul(adaptation, rgbXYZ, coeffs); |
| |
| return AVIF_TRUE; |
| } |
| |
| avifBool avifColorPrimariesComputeXYZD50ToRGBMatrix(avifColorPrimaries colorPrimaries, double coeffs[3][3]) |
| { |
| double rgbToXyz[3][3]; |
| AVIF_CHECK(avifColorPrimariesComputeRGBToXYZD50Matrix(colorPrimaries, rgbToXyz)); |
| AVIF_CHECK(avifMatInv(rgbToXyz, coeffs)); |
| return AVIF_TRUE; |
| } |
| |
| avifBool avifColorPrimariesComputeRGBToRGBMatrix(avifColorPrimaries srcColorPrimaries, |
| avifColorPrimaries dstColorPrimaries, |
| double coeffs[3][3]) |
| { |
| // Note: no special casing for srcColorPrimaries == dstColorPrimaries to allow |
| // testing that the computation actually produces the identity matrix. |
| double srcRGBToXYZ[3][3]; |
| AVIF_CHECK(avifColorPrimariesComputeRGBToXYZD50Matrix(srcColorPrimaries, srcRGBToXYZ)); |
| double xyzToDstRGB[3][3]; |
| AVIF_CHECK(avifColorPrimariesComputeXYZD50ToRGBMatrix(dstColorPrimaries, xyzToDstRGB)); |
| // coeffs = xyzToDstRGB * srcRGBToXYZ |
| // i.e. srcRGB -> XYZ -> dstRGB |
| avifMatMul(xyzToDstRGB, srcRGBToXYZ, coeffs); |
| return AVIF_TRUE; |
| } |
| |
| // Converts a linear RGBA pixel to a different color space. This function actually works for gamma encoded |
| // RGB as well but linear gives better results. Also, for gamma encoded values, it would be |
| // better to clamp the output to [0, 1]. Linear values don't need clamping because values |
| // > 1.0 are valid for HDR transfer curves, and the gamma compression function will do the |
| // clamping as necessary. |
| void avifLinearRGBConvertColorSpace(float rgb[4], double coeffs[3][3]) |
| { |
| const double rgbDouble[3] = { rgb[0], rgb[1], rgb[2] }; |
| double converted[3]; |
| avifVecMul(coeffs, rgbDouble, converted); |
| rgb[0] = (float)converted[0]; |
| rgb[1] = (float)converted[1]; |
| rgb[2] = (float)converted[2]; |
| } |