|
|
|
/*
|
|
|
|
* Copyright (c) 2022 Niklas Haas
|
|
|
|
* This file is part of FFmpeg.
|
|
|
|
*
|
|
|
|
* FFmpeg is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
|
|
* License as published by the Free Software Foundation; either
|
|
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* FFmpeg is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* Lesser General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with FFmpeg; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "libavutil/csp.h"
|
|
|
|
|
|
|
|
#include "fflcms2.h"
|
|
|
|
|
|
|
|
static void log_cb(cmsContext ctx, cmsUInt32Number error, const char *str)
|
|
|
|
{
|
|
|
|
FFIccContext *s = cmsGetContextUserData(ctx);
|
|
|
|
av_log(s->avctx, AV_LOG_ERROR, "lcms2: [%"PRIu32"] %s\n", error, str);
|
|
|
|
}
|
|
|
|
|
|
|
|
int ff_icc_context_init(FFIccContext *s, void *avctx)
|
|
|
|
{
|
|
|
|
memset(s, 0, sizeof(*s));
|
|
|
|
s->avctx = avctx;
|
|
|
|
s->ctx = cmsCreateContext(NULL, s);
|
|
|
|
if (!s->ctx)
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
|
|
|
|
cmsSetLogErrorHandlerTHR(s->ctx, log_cb);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ff_icc_context_uninit(FFIccContext *s)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < FF_ARRAY_ELEMS(s->curves); i++)
|
|
|
|
cmsFreeToneCurve(s->curves[i]);
|
|
|
|
cmsDeleteContext(s->ctx);
|
|
|
|
memset(s, 0, sizeof(*s));
|
|
|
|
}
|
|
|
|
|
|
|
|
static int get_curve(FFIccContext *s, enum AVColorTransferCharacteristic trc,
|
|
|
|
cmsToneCurve **out_curve)
|
|
|
|
{
|
|
|
|
if (trc >= AVCOL_TRC_NB)
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
|
|
|
|
if (s->curves[trc])
|
|
|
|
goto done;
|
|
|
|
|
|
|
|
switch (trc) {
|
|
|
|
case AVCOL_TRC_LINEAR:
|
|
|
|
s->curves[trc] = cmsBuildGamma(s->ctx, 1.0);
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_GAMMA22:
|
|
|
|
s->curves[trc] = cmsBuildGamma(s->ctx, 2.2);
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_GAMMA28:
|
|
|
|
s->curves[trc] = cmsBuildGamma(s->ctx, 2.8);
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_BT709:
|
|
|
|
case AVCOL_TRC_SMPTE170M:
|
|
|
|
case AVCOL_TRC_BT2020_10:
|
|
|
|
case AVCOL_TRC_BT2020_12:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
|
|
|
|
/* γ = */ 1/0.45,
|
|
|
|
/* a = */ 1/1.099296826809442,
|
|
|
|
/* b = */ 1 - 1/1.099296826809442,
|
|
|
|
/* c = */ 1/4.5,
|
|
|
|
/* d = */ 4.5 * 0.018053968510807,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_SMPTE240M:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
|
|
|
|
/* γ = */ 1/0.45,
|
|
|
|
/* a = */ 1/1.1115,
|
|
|
|
/* b = */ 1 - 1/1.1115,
|
|
|
|
/* c = */ 1/4.0,
|
|
|
|
/* d = */ 4.0 * 0.0228,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_LOG:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
|
|
|
|
/* a = */ 1.0,
|
|
|
|
/* b = */ 10.0,
|
|
|
|
/* c = */ 2.0,
|
|
|
|
/* d = */ -1.0,
|
|
|
|
/* e = */ 0.0
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_LOG_SQRT:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
|
|
|
|
/* a = */ 1.0,
|
|
|
|
/* b = */ 10.0,
|
|
|
|
/* c = */ 2.5,
|
|
|
|
/* d = */ -1.0,
|
|
|
|
/* e = */ 0.0
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_IEC61966_2_1:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
|
|
|
|
/* γ = */ 2.4,
|
|
|
|
/* a = */ 1/1.055,
|
|
|
|
/* b = */ 1 - 1/1.055,
|
|
|
|
/* c = */ 1/12.92,
|
|
|
|
/* d = */ 12.92 * 0.0031308,
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
case AVCOL_TRC_SMPTE428:
|
|
|
|
s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 2, (double[3]) {
|
|
|
|
/* γ = */ 2.6,
|
|
|
|
/* a = */ pow(52.37/48.0, 1/2.6),
|
|
|
|
/* b = */ 0.0
|
|
|
|
});
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* Can't be represented using the existing parametric tone curves.
|
|
|
|
* FIXME: use cmsBuildTabulatedToneCurveFloat instead */
|
|
|
|
case AVCOL_TRC_IEC61966_2_4:
|
|
|
|
case AVCOL_TRC_BT1361_ECG:
|
|
|
|
case AVCOL_TRC_SMPTE2084:
|
|
|
|
case AVCOL_TRC_ARIB_STD_B67:
|
|
|
|
return AVERROR_PATCHWELCOME;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!s->curves[trc])
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
|
|
|
|
done:
|
|
|
|
*out_curve = s->curves[trc];
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ff_icc_profile_generate(FFIccContext *s,
|
|
|
|
enum AVColorPrimaries color_prim,
|
|
|
|
enum AVColorTransferCharacteristic color_trc,
|
|
|
|
cmsHPROFILE *out_profile)
|
|
|
|
{
|
|
|
|
cmsToneCurve *tonecurve;
|
|
|
|
const AVColorPrimariesDesc *prim;
|
|
|
|
int ret;
|
|
|
|
|
|
|
|
if (!(prim = av_csp_primaries_desc_from_id(color_prim)))
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
if ((ret = get_curve(s, color_trc, &tonecurve)) < 0)
|
|
|
|
return ret;
|
|
|
|
|
|
|
|
*out_profile = cmsCreateRGBProfileTHR(s->ctx,
|
|
|
|
&(cmsCIExyY) { av_q2d(prim->wp.x), av_q2d(prim->wp.y), 1.0 },
|
|
|
|
&(cmsCIExyYTRIPLE) {
|
|
|
|
.Red = { av_q2d(prim->prim.r.x), av_q2d(prim->prim.r.y), 1.0 },
|
|
|
|
.Green = { av_q2d(prim->prim.g.x), av_q2d(prim->prim.g.y), 1.0 },
|
|
|
|
.Blue = { av_q2d(prim->prim.b.x), av_q2d(prim->prim.b.y), 1.0 },
|
|
|
|
},
|
|
|
|
(cmsToneCurve *[3]) { tonecurve, tonecurve, tonecurve }
|
|
|
|
);
|
|
|
|
|
|
|
|
return *out_profile == NULL ? AVERROR(ENOMEM) : 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame)
|
|
|
|
{
|
|
|
|
cmsUInt32Number size;
|
|
|
|
AVBufferRef *buf;
|
|
|
|
|
|
|
|
if (!cmsSaveProfileToMem(profile, NULL, &size))
|
|
|
|
return AVERROR_EXTERNAL;
|
|
|
|
|
|
|
|
buf = av_buffer_alloc(size);
|
|
|
|
if (!buf)
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
|
|
|
|
if (!cmsSaveProfileToMem(profile, buf->data, &size) || size != buf->size) {
|
|
|
|
av_buffer_unref(&buf);
|
|
|
|
return AVERROR_EXTERNAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_ICC_PROFILE, buf)) {
|
|
|
|
av_buffer_unref(&buf);
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static av_always_inline void XYZ_xy(cmsCIEXYZ XYZ, AVCIExy *xy)
|
|
|
|
{
|
|
|
|
double k = 1.0 / (XYZ.X + XYZ.Y + XYZ.Z);
|
|
|
|
xy->x = av_d2q(k * XYZ.X, 100000);
|
|
|
|
xy->y = av_d2q(k * XYZ.Y, 100000);
|
|
|
|
}
|
|
|
|
|
|
|
|
static av_always_inline AVRational abs_sub_q(AVRational r1, AVRational r2)
|
|
|
|
{
|
|
|
|
AVRational diff = av_sub_q(r1, r2);
|
|
|
|
/* denominator assumed to be positive */
|
|
|
|
return av_make_q(abs(diff.num), diff.den);
|
|
|
|
}
|
|
|
|
|
|
|
|
static const AVCIExy wp_d50 = { {3457, 10000}, {3585, 10000} }; /* CIE D50 */
|
|
|
|
|
|
|
|
int ff_icc_profile_sanitize(FFIccContext *s, cmsHPROFILE profile)
|
|
|
|
{
|
|
|
|
cmsCIEXYZ *white, fixed;
|
|
|
|
AVCIExy wpxy;
|
|
|
|
AVRational diff, z;
|
|
|
|
if (!profile)
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
if (cmsGetEncodedICCversion(profile) >= 0x4000000) { // ICC v4
|
|
|
|
switch (cmsGetHeaderRenderingIntent(profile)) {
|
|
|
|
case INTENT_RELATIVE_COLORIMETRIC:
|
|
|
|
case INTENT_ABSOLUTE_COLORIMETRIC: ;
|
|
|
|
/* ICC v4 colorimetric profiles are specified to always use D50
|
|
|
|
* media white point, anything else is a violation of the spec.
|
|
|
|
* Sadly, such profiles are incredibly common (Apple...), so make
|
|
|
|
* an effort to fix them. */
|
|
|
|
if (!(white = cmsReadTag(profile, cmsSigMediaWhitePointTag)))
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
XYZ_xy(*white, &wpxy);
|
|
|
|
diff = av_add_q(abs_sub_q(wpxy.x, wp_d50.x), abs_sub_q(wpxy.y, wp_d50.y));
|
|
|
|
if (av_cmp_q(diff, av_make_q(1, 1000)) > 0) {
|
|
|
|
av_log(s->avctx, AV_LOG_WARNING, "Invalid colorimetric ICCv4 "
|
|
|
|
"profile media white point tag (expected %.4f %.4f, "
|
|
|
|
"got %.4f %.4f)\n",
|
|
|
|
av_q2d(wp_d50.x), av_q2d(wp_d50.y),
|
|
|
|
av_q2d(wpxy.x), av_q2d(wpxy.y));
|
|
|
|
/* x+y+z = 1 */
|
|
|
|
z = av_sub_q(av_sub_q(av_make_q(1, 1), wp_d50.x), wp_d50.y);
|
|
|
|
fixed.X = av_q2d(av_div_q(wp_d50.x, wp_d50.y)) * white->Y;
|
|
|
|
fixed.Y = white->Y;
|
|
|
|
fixed.Z = av_q2d(av_div_q(z, wp_d50.y)) * white->Y;
|
|
|
|
if (!cmsWriteTag(profile, cmsSigMediaWhitePointTag, &fixed))
|
|
|
|
return AVERROR_EXTERNAL;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default: break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
|
|
|
|
AVColorPrimariesDesc *out_primaries)
|
|
|
|
{
|
|
|
|
static const uint8_t testprimaries[4][3] = {
|
|
|
|
{ 0xFF, 0, 0 }, /* red */
|
|
|
|
{ 0, 0xFF, 0 }, /* green */
|
|
|
|
{ 0, 0, 0xFF }, /* blue */
|
|
|
|
{ 0xFF, 0xFF, 0xFF }, /* white */
|
|
|
|
};
|
|
|
|
|
|
|
|
AVWhitepointCoefficients *wp = &out_primaries->wp;
|
|
|
|
AVPrimaryCoefficients *prim = &out_primaries->prim;
|
|
|
|
cmsFloat64Number prev_adapt;
|
|
|
|
cmsHPROFILE xyz;
|
|
|
|
cmsHTRANSFORM tf;
|
|
|
|
cmsCIEXYZ dst[4];
|
|
|
|
|
|
|
|
xyz = cmsCreateXYZProfileTHR(s->ctx);
|
|
|
|
if (!xyz)
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
|
|
|
|
/* We need to use an unadapted observer to get the raw values */
|
|
|
|
prev_adapt = cmsSetAdaptationStateTHR(s->ctx, 0.0);
|
|
|
|
tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, xyz, TYPE_XYZ_DBL,
|
|
|
|
INTENT_ABSOLUTE_COLORIMETRIC,
|
|
|
|
/* Note: These flags mostly don't do anything
|
|
|
|
* anyway, but specify them regardless */
|
|
|
|
cmsFLAGS_NOCACHE |
|
|
|
|
cmsFLAGS_NOOPTIMIZE |
|
|
|
|
cmsFLAGS_LOWRESPRECALC |
|
|
|
|
cmsFLAGS_GRIDPOINTS(2));
|
|
|
|
cmsSetAdaptationStateTHR(s->ctx, prev_adapt);
|
|
|
|
cmsCloseProfile(xyz);
|
|
|
|
if (!tf) {
|
|
|
|
av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
}
|
|
|
|
|
|
|
|
cmsDoTransform(tf, testprimaries, dst, 4);
|
|
|
|
cmsDeleteTransform(tf);
|
|
|
|
XYZ_xy(dst[0], &prim->r);
|
|
|
|
XYZ_xy(dst[1], &prim->g);
|
|
|
|
XYZ_xy(dst[2], &prim->b);
|
|
|
|
XYZ_xy(dst[3], wp);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
|
|
|
|
enum AVColorTransferCharacteristic *out_trc)
|
|
|
|
{
|
|
|
|
/* 8-bit linear grayscale ramp */
|
|
|
|
static const uint8_t testramp[16][3] = {
|
|
|
|
{ 1, 1, 1}, /* avoid exact zero due to log100 etc. */
|
|
|
|
{ 17, 17, 17},
|
|
|
|
{ 34, 34, 34},
|
|
|
|
{ 51, 51, 51},
|
|
|
|
{ 68, 68, 68},
|
|
|
|
{ 85, 85, 85},
|
|
|
|
{ 02, 02, 02},
|
|
|
|
{119, 119, 119},
|
|
|
|
{136, 136, 136},
|
|
|
|
{153, 153, 153},
|
|
|
|
{170, 170, 170},
|
|
|
|
{187, 187, 187},
|
|
|
|
{204, 204, 204},
|
|
|
|
{221, 221, 221},
|
|
|
|
{238, 238, 238},
|
|
|
|
{255, 255, 255},
|
|
|
|
};
|
|
|
|
|
|
|
|
double dst[FF_ARRAY_ELEMS(testramp)];
|
|
|
|
|
|
|
|
for (enum AVColorTransferCharacteristic trc = 0; trc < AVCOL_TRC_NB; trc++) {
|
|
|
|
cmsToneCurve *tonecurve;
|
|
|
|
cmsHPROFILE ref;
|
|
|
|
cmsHTRANSFORM tf;
|
|
|
|
double delta = 0.0;
|
|
|
|
if (get_curve(s, trc, &tonecurve) < 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
ref = cmsCreateGrayProfileTHR(s->ctx, cmsD50_xyY(), tonecurve);
|
|
|
|
if (!ref)
|
|
|
|
return AVERROR(ENOMEM);
|
|
|
|
|
|
|
|
tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, ref, TYPE_GRAY_DBL,
|
|
|
|
INTENT_RELATIVE_COLORIMETRIC,
|
|
|
|
cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
|
|
|
|
cmsCloseProfile(ref);
|
|
|
|
if (!tf) {
|
|
|
|
av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
|
|
|
|
return AVERROR_INVALIDDATA;
|
|
|
|
}
|
|
|
|
|
|
|
|
cmsDoTransform(tf, testramp, dst, FF_ARRAY_ELEMS(dst));
|
|
|
|
cmsDeleteTransform(tf);
|
|
|
|
|
|
|
|
for (int i = 0; i < FF_ARRAY_ELEMS(dst); i++)
|
|
|
|
delta += fabs(testramp[i][0] / 255.0 - dst[i]);
|
|
|
|
if (delta < 0.01) {
|
|
|
|
*out_trc = trc;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
*out_trc = AVCOL_TRC_UNSPECIFIED;
|
|
|
|
return 0;
|
|
|
|
}
|