2 * Copyright (c) 2022 Niklas Haas
3 * This file is part of FFmpeg.
5 * FFmpeg is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * FFmpeg is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with FFmpeg; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 #include "libavutil/csp.h"
24 static void log_cb(cmsContext ctx
, cmsUInt32Number error
, const char *str
)
26 FFIccContext
*s
= cmsGetContextUserData(ctx
);
27 av_log(s
->avctx
, AV_LOG_ERROR
, "lcms2: [%"PRIu32
"] %s\n", error
, str
);
30 int ff_icc_context_init(FFIccContext
*s
, void *avctx
)
32 memset(s
, 0, sizeof(*s
));
34 s
->ctx
= cmsCreateContext(NULL
, s
);
36 return AVERROR(ENOMEM
);
38 cmsSetLogErrorHandlerTHR(s
->ctx
, log_cb
);
42 void ff_icc_context_uninit(FFIccContext
*s
)
44 for (int i
= 0; i
< FF_ARRAY_ELEMS(s
->curves
); i
++)
45 cmsFreeToneCurve(s
->curves
[i
]);
46 cmsDeleteContext(s
->ctx
);
47 memset(s
, 0, sizeof(*s
));
50 static int get_curve(FFIccContext
*s
, enum AVColorTransferCharacteristic trc
,
51 cmsToneCurve
**out_curve
)
53 if (trc
>= AVCOL_TRC_NB
)
54 return AVERROR_INVALIDDATA
;
60 case AVCOL_TRC_LINEAR
:
61 s
->curves
[trc
] = cmsBuildGamma(s
->ctx
, 1.0);
63 case AVCOL_TRC_GAMMA22
:
64 s
->curves
[trc
] = cmsBuildGamma(s
->ctx
, 2.2);
66 case AVCOL_TRC_GAMMA28
:
67 s
->curves
[trc
] = cmsBuildGamma(s
->ctx
, 2.8);
70 case AVCOL_TRC_SMPTE170M
:
71 case AVCOL_TRC_BT2020_10
:
72 case AVCOL_TRC_BT2020_12
:
73 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 4, (double[5]) {
75 /* a = */ 1/1.099296826809442,
76 /* b = */ 1 - 1/1.099296826809442,
78 /* d = */ 4.5 * 0.018053968510807,
81 case AVCOL_TRC_SMPTE240M
:
82 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 4, (double[5]) {
85 /* b = */ 1 - 1/1.1115,
87 /* d = */ 4.0 * 0.0228,
91 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 8, (double[5]) {
99 case AVCOL_TRC_LOG_SQRT
:
100 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 8, (double[5]) {
108 case AVCOL_TRC_IEC61966_2_1
:
109 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 4, (double[5]) {
112 /* b = */ 1 - 1/1.055,
114 /* d = */ 12.92 * 0.0031308,
117 case AVCOL_TRC_SMPTE428
:
118 s
->curves
[trc
] = cmsBuildParametricToneCurve(s
->ctx
, 2, (double[3]) {
120 /* a = */ pow(52.37/48.0, 1/2.6),
125 /* Can't be represented using the existing parametric tone curves.
126 * FIXME: use cmsBuildTabulatedToneCurveFloat instead */
127 case AVCOL_TRC_IEC61966_2_4
:
128 case AVCOL_TRC_BT1361_ECG
:
129 case AVCOL_TRC_SMPTE2084
:
130 case AVCOL_TRC_ARIB_STD_B67
:
131 return AVERROR_PATCHWELCOME
;
134 return AVERROR_INVALIDDATA
;
138 return AVERROR(ENOMEM
);
141 *out_curve
= s
->curves
[trc
];
145 int ff_icc_profile_generate(FFIccContext
*s
,
146 enum AVColorPrimaries color_prim
,
147 enum AVColorTransferCharacteristic color_trc
,
148 cmsHPROFILE
*out_profile
)
150 cmsToneCurve
*tonecurve
;
151 const AVColorPrimariesDesc
*prim
;
154 if (!(prim
= av_csp_primaries_desc_from_id(color_prim
)))
155 return AVERROR_INVALIDDATA
;
156 if ((ret
= get_curve(s
, color_trc
, &tonecurve
)) < 0)
159 *out_profile
= cmsCreateRGBProfileTHR(s
->ctx
,
160 &(cmsCIExyY
) { av_q2d(prim
->wp
.x
), av_q2d(prim
->wp
.y
), 1.0 },
162 .Red
= { av_q2d(prim
->prim
.r
.x
), av_q2d(prim
->prim
.r
.y
), 1.0 },
163 .Green
= { av_q2d(prim
->prim
.g
.x
), av_q2d(prim
->prim
.g
.y
), 1.0 },
164 .Blue
= { av_q2d(prim
->prim
.b
.x
), av_q2d(prim
->prim
.b
.y
), 1.0 },
166 (cmsToneCurve
*[3]) { tonecurve
, tonecurve
, tonecurve
}
169 return *out_profile
== NULL
? AVERROR(ENOMEM
) : 0;
172 int ff_icc_profile_attach(FFIccContext
*s
, cmsHPROFILE profile
, AVFrame
*frame
)
174 cmsUInt32Number size
;
177 if (!cmsSaveProfileToMem(profile
, NULL
, &size
))
178 return AVERROR_EXTERNAL
;
180 buf
= av_buffer_alloc(size
);
182 return AVERROR(ENOMEM
);
184 if (!cmsSaveProfileToMem(profile
, buf
->data
, &size
) || size
!= buf
->size
) {
185 av_buffer_unref(&buf
);
186 return AVERROR_EXTERNAL
;
189 if (!av_frame_new_side_data_from_buf(frame
, AV_FRAME_DATA_ICC_PROFILE
, buf
)) {
190 av_buffer_unref(&buf
);
191 return AVERROR(ENOMEM
);
197 static av_always_inline
void XYZ_xy(cmsCIEXYZ XYZ
, AVCIExy
*xy
)
199 double k
= 1.0 / (XYZ
.X
+ XYZ
.Y
+ XYZ
.Z
);
200 xy
->x
= av_d2q(k
* XYZ
.X
, 100000);
201 xy
->y
= av_d2q(k
* XYZ
.Y
, 100000);
204 static av_always_inline AVRational
abs_sub_q(AVRational r1
, AVRational r2
)
206 AVRational diff
= av_sub_q(r1
, r2
);
207 /* denominator assumed to be positive */
208 return av_make_q(abs(diff
.num
), diff
.den
);
211 static const AVCIExy wp_d50
= { {3457, 10000}, {3585, 10000} }; /* CIE D50 */
213 int ff_icc_profile_sanitize(FFIccContext
*s
, cmsHPROFILE profile
)
215 cmsCIEXYZ
*white
, fixed
;
221 if (cmsGetEncodedICCversion(profile
) >= 0x4000000) { // ICC v4
222 switch (cmsGetHeaderRenderingIntent(profile
)) {
223 case INTENT_RELATIVE_COLORIMETRIC
:
224 case INTENT_ABSOLUTE_COLORIMETRIC
: ;
225 /* ICC v4 colorimetric profiles are specified to always use D50
226 * media white point, anything else is a violation of the spec.
227 * Sadly, such profiles are incredibly common (Apple...), so make
228 * an effort to fix them. */
229 if (!(white
= cmsReadTag(profile
, cmsSigMediaWhitePointTag
)))
230 return AVERROR_INVALIDDATA
;
231 XYZ_xy(*white
, &wpxy
);
232 diff
= av_add_q(abs_sub_q(wpxy
.x
, wp_d50
.x
), abs_sub_q(wpxy
.y
, wp_d50
.y
));
233 if (av_cmp_q(diff
, av_make_q(1, 1000)) > 0) {
234 av_log(s
->avctx
, AV_LOG_WARNING
, "Invalid colorimetric ICCv4 "
235 "profile media white point tag (expected %.4f %.4f, "
237 av_q2d(wp_d50
.x
), av_q2d(wp_d50
.y
),
238 av_q2d(wpxy
.x
), av_q2d(wpxy
.y
));
240 z
= av_sub_q(av_sub_q(av_make_q(1, 1), wp_d50
.x
), wp_d50
.y
);
241 fixed
.X
= av_q2d(av_div_q(wp_d50
.x
, wp_d50
.y
)) * white
->Y
;
243 fixed
.Z
= av_q2d(av_div_q(z
, wp_d50
.y
)) * white
->Y
;
244 if (!cmsWriteTag(profile
, cmsSigMediaWhitePointTag
, &fixed
))
245 return AVERROR_EXTERNAL
;
255 int ff_icc_profile_read_primaries(FFIccContext
*s
, cmsHPROFILE profile
,
256 AVColorPrimariesDesc
*out_primaries
)
258 static const uint8_t testprimaries
[4][3] = {
259 { 0xFF, 0, 0 }, /* red */
260 { 0, 0xFF, 0 }, /* green */
261 { 0, 0, 0xFF }, /* blue */
262 { 0xFF, 0xFF, 0xFF }, /* white */
265 AVWhitepointCoefficients
*wp
= &out_primaries
->wp
;
266 AVPrimaryCoefficients
*prim
= &out_primaries
->prim
;
267 cmsFloat64Number prev_adapt
;
272 xyz
= cmsCreateXYZProfileTHR(s
->ctx
);
274 return AVERROR(ENOMEM
);
276 /* We need to use an unadapted observer to get the raw values */
277 prev_adapt
= cmsSetAdaptationStateTHR(s
->ctx
, 0.0);
278 tf
= cmsCreateTransformTHR(s
->ctx
, profile
, TYPE_RGB_8
, xyz
, TYPE_XYZ_DBL
,
279 INTENT_ABSOLUTE_COLORIMETRIC
,
280 /* Note: These flags mostly don't do anything
281 * anyway, but specify them regardless */
283 cmsFLAGS_NOOPTIMIZE
|
284 cmsFLAGS_LOWRESPRECALC
|
285 cmsFLAGS_GRIDPOINTS(2));
286 cmsSetAdaptationStateTHR(s
->ctx
, prev_adapt
);
287 cmsCloseProfile(xyz
);
289 av_log(s
->avctx
, AV_LOG_ERROR
, "Invalid ICC profile (e.g. CMYK)\n");
290 return AVERROR_INVALIDDATA
;
293 cmsDoTransform(tf
, testprimaries
, dst
, 4);
294 cmsDeleteTransform(tf
);
295 XYZ_xy(dst
[0], &prim
->r
);
296 XYZ_xy(dst
[1], &prim
->g
);
297 XYZ_xy(dst
[2], &prim
->b
);
302 int ff_icc_profile_detect_transfer(FFIccContext
*s
, cmsHPROFILE profile
,
303 enum AVColorTransferCharacteristic
*out_trc
)
305 /* 8-bit linear grayscale ramp */
306 static const uint8_t testramp
[16][3] = {
307 { 1, 1, 1}, /* avoid exact zero due to log100 etc. */
325 double dst
[FF_ARRAY_ELEMS(testramp
)];
327 for (enum AVColorTransferCharacteristic trc
= 0; trc
< AVCOL_TRC_NB
; trc
++) {
328 cmsToneCurve
*tonecurve
;
332 if (get_curve(s
, trc
, &tonecurve
) < 0)
335 ref
= cmsCreateGrayProfileTHR(s
->ctx
, cmsD50_xyY(), tonecurve
);
337 return AVERROR(ENOMEM
);
339 tf
= cmsCreateTransformTHR(s
->ctx
, profile
, TYPE_RGB_8
, ref
, TYPE_GRAY_DBL
,
340 INTENT_RELATIVE_COLORIMETRIC
,
341 cmsFLAGS_NOCACHE
| cmsFLAGS_NOOPTIMIZE
);
342 cmsCloseProfile(ref
);
344 av_log(s
->avctx
, AV_LOG_ERROR
, "Invalid ICC profile (e.g. CMYK)\n");
345 return AVERROR_INVALIDDATA
;
348 cmsDoTransform(tf
, testramp
, dst
, FF_ARRAY_ELEMS(dst
));
349 cmsDeleteTransform(tf
);
351 for (int i
= 0; i
< FF_ARRAY_ELEMS(dst
); i
++)
352 delta
+= fabs(testramp
[i
][0] / 255.0 - dst
[i
]);
359 *out_trc
= AVCOL_TRC_UNSPECIFIED
;