1 /*
2 * Copyright (c) 2022 Niklas Haas
3 * This file is part of FFmpeg.
4 *
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.
9 *
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.
14 *
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
18 */
19
20 #include "libavutil/color_utils.h"
21 #include "libavutil/csp.h"
22
23 #include "fflcms2.h"
24
log_cb(cmsContext ctx,cmsUInt32Number error,const char * str)25 static void log_cb(cmsContext ctx, cmsUInt32Number error, const char *str)
26 {
27 FFIccContext *s = cmsGetContextUserData(ctx);
28 av_log(s->avctx, AV_LOG_ERROR, "lcms2: [%"PRIu32"] %s\n", error, str);
29 }
30
ff_icc_context_init(FFIccContext * s,void * avctx)31 int ff_icc_context_init(FFIccContext *s, void *avctx)
32 {
33 memset(s, 0, sizeof(*s));
34 s->avctx = avctx;
35 s->ctx = cmsCreateContext(NULL, s);
36 if (!s->ctx)
37 return AVERROR(ENOMEM);
38
39 cmsSetLogErrorHandlerTHR(s->ctx, log_cb);
40 return 0;
41 }
42
ff_icc_context_uninit(FFIccContext * s)43 void ff_icc_context_uninit(FFIccContext *s)
44 {
45 for (int i = 0; i < FF_ARRAY_ELEMS(s->curves); i++)
46 cmsFreeToneCurve(s->curves[i]);
47 cmsDeleteContext(s->ctx);
48 memset(s, 0, sizeof(*s));
49 }
50
get_curve(FFIccContext * s,enum AVColorTransferCharacteristic trc,cmsToneCurve ** out_curve)51 static int get_curve(FFIccContext *s, enum AVColorTransferCharacteristic trc,
52 cmsToneCurve **out_curve)
53 {
54 if (trc >= AVCOL_TRC_NB)
55 return AVERROR_INVALIDDATA;
56
57 if (s->curves[trc])
58 goto done;
59
60 switch (trc) {
61 case AVCOL_TRC_LINEAR:
62 s->curves[trc] = cmsBuildGamma(s->ctx, 1.0);
63 break;
64 case AVCOL_TRC_GAMMA22:
65 s->curves[trc] = cmsBuildGamma(s->ctx, 2.2);
66 break;
67 case AVCOL_TRC_GAMMA28:
68 s->curves[trc] = cmsBuildGamma(s->ctx, 2.8);
69 break;
70 case AVCOL_TRC_BT709:
71 case AVCOL_TRC_SMPTE170M:
72 case AVCOL_TRC_BT2020_10:
73 case AVCOL_TRC_BT2020_12:
74 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
75 /* γ = */ 1/0.45,
76 /* a = */ 1/1.099296826809442,
77 /* b = */ 1 - 1/1.099296826809442,
78 /* c = */ 1/4.5,
79 /* d = */ 4.5 * 0.018053968510807,
80 });
81 break;
82 case AVCOL_TRC_SMPTE240M:
83 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
84 /* γ = */ 1/0.45,
85 /* a = */ 1/1.1115,
86 /* b = */ 1 - 1/1.1115,
87 /* c = */ 1/4.0,
88 /* d = */ 4.0 * 0.0228,
89 });
90 break;
91 case AVCOL_TRC_LOG:
92 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
93 /* a = */ 1.0,
94 /* b = */ 10.0,
95 /* c = */ 2.0,
96 /* d = */ -1.0,
97 /* e = */ 0.0
98 });
99 break;
100 case AVCOL_TRC_LOG_SQRT:
101 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 8, (double[5]) {
102 /* a = */ 1.0,
103 /* b = */ 10.0,
104 /* c = */ 2.5,
105 /* d = */ -1.0,
106 /* e = */ 0.0
107 });
108 break;
109 case AVCOL_TRC_IEC61966_2_1:
110 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 4, (double[5]) {
111 /* γ = */ 2.4,
112 /* a = */ 1/1.055,
113 /* b = */ 1 - 1/1.055,
114 /* c = */ 1/12.92,
115 /* d = */ 12.92 * 0.0031308,
116 });
117 break;
118 case AVCOL_TRC_SMPTE428:
119 s->curves[trc] = cmsBuildParametricToneCurve(s->ctx, 2, (double[3]) {
120 /* γ = */ 2.6,
121 /* a = */ pow(52.37/48.0, 1/2.6),
122 /* b = */ 0.0
123 });
124 break;
125
126 /* Can't be represented using the existing parametric tone curves.
127 * FIXME: use cmsBuildTabulatedToneCurveFloat instead */
128 case AVCOL_TRC_IEC61966_2_4:
129 case AVCOL_TRC_BT1361_ECG:
130 case AVCOL_TRC_SMPTE2084:
131 case AVCOL_TRC_ARIB_STD_B67:
132 return AVERROR_PATCHWELCOME;
133
134 default:
135 return AVERROR_INVALIDDATA;
136 }
137
138 if (!s->curves[trc])
139 return AVERROR(ENOMEM);
140
141 done:
142 *out_curve = s->curves[trc];
143 return 0;
144 }
145
ff_icc_profile_generate(FFIccContext * s,enum AVColorPrimaries color_prim,enum AVColorTransferCharacteristic color_trc,cmsHPROFILE * out_profile)146 int ff_icc_profile_generate(FFIccContext *s,
147 enum AVColorPrimaries color_prim,
148 enum AVColorTransferCharacteristic color_trc,
149 cmsHPROFILE *out_profile)
150 {
151 cmsToneCurve *tonecurve;
152 const AVColorPrimariesDesc *prim;
153 int ret;
154
155 if (!(prim = av_csp_primaries_desc_from_id(color_prim)))
156 return AVERROR_INVALIDDATA;
157 if ((ret = get_curve(s, color_trc, &tonecurve)) < 0)
158 return ret;
159
160 *out_profile = cmsCreateRGBProfileTHR(s->ctx,
161 &(cmsCIExyY) { av_q2d(prim->wp.x), av_q2d(prim->wp.y), 1.0 },
162 &(cmsCIExyYTRIPLE) {
163 .Red = { av_q2d(prim->prim.r.x), av_q2d(prim->prim.r.y), 1.0 },
164 .Green = { av_q2d(prim->prim.g.x), av_q2d(prim->prim.g.y), 1.0 },
165 .Blue = { av_q2d(prim->prim.b.x), av_q2d(prim->prim.b.y), 1.0 },
166 },
167 (cmsToneCurve *[3]) { tonecurve, tonecurve, tonecurve }
168 );
169
170 return *out_profile == NULL ? AVERROR(ENOMEM) : 0;
171 }
172
ff_icc_profile_attach(FFIccContext * s,cmsHPROFILE profile,AVFrame * frame)173 int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame)
174 {
175 cmsUInt32Number size;
176 AVBufferRef *buf;
177
178 if (!cmsSaveProfileToMem(profile, NULL, &size))
179 return AVERROR_EXTERNAL;
180
181 buf = av_buffer_alloc(size);
182 if (!buf)
183 return AVERROR(ENOMEM);
184
185 if (!cmsSaveProfileToMem(profile, buf->data, &size) || size != buf->size) {
186 av_buffer_unref(&buf);
187 return AVERROR_EXTERNAL;
188 }
189
190 if (!av_frame_new_side_data_from_buf(frame, AV_FRAME_DATA_ICC_PROFILE, buf)) {
191 av_buffer_unref(&buf);
192 return AVERROR(ENOMEM);
193 }
194
195 return 0;
196 }
197
XYZ_xy(cmsCIEXYZ XYZ,AVCIExy * xy)198 static av_always_inline void XYZ_xy(cmsCIEXYZ XYZ, AVCIExy *xy)
199 {
200 double k = 1.0 / (XYZ.X + XYZ.Y + XYZ.Z);
201 xy->x = av_d2q(k * XYZ.X, 100000);
202 xy->y = av_d2q(k * XYZ.Y, 100000);
203 }
204
ff_icc_profile_read_primaries(FFIccContext * s,cmsHPROFILE profile,AVColorPrimariesDesc * out_primaries)205 int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
206 AVColorPrimariesDesc *out_primaries)
207 {
208 static const uint8_t testprimaries[4][3] = {
209 { 0xFF, 0, 0 }, /* red */
210 { 0, 0xFF, 0 }, /* green */
211 { 0, 0, 0xFF }, /* blue */
212 { 0xFF, 0xFF, 0xFF }, /* white */
213 };
214
215 AVWhitepointCoefficients *wp = &out_primaries->wp;
216 AVPrimaryCoefficients *prim = &out_primaries->prim;
217 cmsFloat64Number prev_adapt;
218 cmsHPROFILE xyz;
219 cmsHTRANSFORM tf;
220 cmsCIEXYZ dst[4];
221
222 xyz = cmsCreateXYZProfileTHR(s->ctx);
223 if (!xyz)
224 return AVERROR(ENOMEM);
225
226 /* We need to use an unadapted observer to get the raw values */
227 prev_adapt = cmsSetAdaptationStateTHR(s->ctx, 0.0);
228 tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, xyz, TYPE_XYZ_DBL,
229 INTENT_ABSOLUTE_COLORIMETRIC,
230 /* Note: These flags mostly don't do anything
231 * anyway, but specify them regardless */
232 cmsFLAGS_NOCACHE |
233 cmsFLAGS_NOOPTIMIZE |
234 cmsFLAGS_LOWRESPRECALC |
235 cmsFLAGS_GRIDPOINTS(2));
236 cmsSetAdaptationStateTHR(s->ctx, prev_adapt);
237 cmsCloseProfile(xyz);
238 if (!tf) {
239 av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
240 return AVERROR_INVALIDDATA;
241 }
242
243 cmsDoTransform(tf, testprimaries, dst, 4);
244 cmsDeleteTransform(tf);
245 XYZ_xy(dst[0], &prim->r);
246 XYZ_xy(dst[1], &prim->g);
247 XYZ_xy(dst[2], &prim->b);
248 XYZ_xy(dst[3], wp);
249 return 0;
250 }
251
ff_icc_profile_detect_transfer(FFIccContext * s,cmsHPROFILE profile,enum AVColorTransferCharacteristic * out_trc)252 int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
253 enum AVColorTransferCharacteristic *out_trc)
254 {
255 /* 8-bit linear grayscale ramp */
256 static const uint8_t testramp[16][3] = {
257 { 1, 1, 1}, /* avoid exact zero due to log100 etc. */
258 { 17, 17, 17},
259 { 34, 34, 34},
260 { 51, 51, 51},
261 { 68, 68, 68},
262 { 85, 85, 85},
263 { 02, 02, 02},
264 {119, 119, 119},
265 {136, 136, 136},
266 {153, 153, 153},
267 {170, 170, 170},
268 {187, 187, 187},
269 {204, 204, 204},
270 {221, 221, 221},
271 {238, 238, 238},
272 {255, 255, 255},
273 };
274
275 double dst[FF_ARRAY_ELEMS(testramp)];
276
277 for (enum AVColorTransferCharacteristic trc = 0; trc < AVCOL_TRC_NB; trc++) {
278 cmsToneCurve *tonecurve;
279 cmsHPROFILE ref;
280 cmsHTRANSFORM tf;
281 double delta = 0.0;
282 if (get_curve(s, trc, &tonecurve) < 0)
283 continue;
284
285 ref = cmsCreateGrayProfileTHR(s->ctx, cmsD50_xyY(), tonecurve);
286 if (!ref)
287 return AVERROR(ENOMEM);
288
289 tf = cmsCreateTransformTHR(s->ctx, profile, TYPE_RGB_8, ref, TYPE_GRAY_DBL,
290 INTENT_RELATIVE_COLORIMETRIC,
291 cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
292 cmsCloseProfile(ref);
293 if (!tf) {
294 av_log(s->avctx, AV_LOG_ERROR, "Invalid ICC profile (e.g. CMYK)\n");
295 return AVERROR_INVALIDDATA;
296 }
297
298 cmsDoTransform(tf, testramp, dst, FF_ARRAY_ELEMS(dst));
299 cmsDeleteTransform(tf);
300
301 for (int i = 0; i < FF_ARRAY_ELEMS(dst); i++)
302 delta += fabs(testramp[i][0] / 255.0 - dst[i]);
303 if (delta < 0.01) {
304 *out_trc = trc;
305 return 0;
306 }
307 }
308
309 *out_trc = AVCOL_TRC_UNSPECIFIED;
310 return 0;
311 }
312