diff --git a/libavcodec/Makefile b/libavcodec/Makefile
index 050934101c..5c4f62c631 100644
--- a/libavcodec/Makefile
+++ b/libavcodec/Makefile
@@ -1235,6 +1235,7 @@ SKIPHEADERS-$(CONFIG_AMF)              += amfenc.h
 SKIPHEADERS-$(CONFIG_D3D11VA)          += d3d11va.h dxva2_internal.h
 SKIPHEADERS-$(CONFIG_DXVA2)            += dxva2.h dxva2_internal.h
 SKIPHEADERS-$(CONFIG_JNI)              += ffjni.h
+SKIPHEADERS-$(CONFIG_LCMS2)            += fflcms2.h
 SKIPHEADERS-$(CONFIG_LIBJXL)           += libjxl.h
 SKIPHEADERS-$(CONFIG_LIBVPX)           += libvpx.h
 SKIPHEADERS-$(CONFIG_LIBWEBP_ENCODER)  += libwebpenc_common.h
diff --git a/libavcodec/fflcms2.c b/libavcodec/fflcms2.c
new file mode 100644
index 0000000000..fd370fb310
--- /dev/null
+++ b/libavcodec/fflcms2.c
@@ -0,0 +1,311 @@
+/*
+ * 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/color_utils.h"
+#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);
+}
+
+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;
+}
diff --git a/libavcodec/fflcms2.h b/libavcodec/fflcms2.h
new file mode 100644
index 0000000000..af63c9a13c
--- /dev/null
+++ b/libavcodec/fflcms2.h
@@ -0,0 +1,87 @@
+/*
+ * 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
+ */
+
+/**
+ * @file
+ * Various functions for dealing with ICC profiles
+ */
+
+#ifndef AVCODEC_FFLCMS2_H
+#define AVCODEC_FFLCMS2_H
+
+#include "libavutil/csp.h"
+#include "libavutil/frame.h"
+#include "libavutil/pixfmt.h"
+
+#include <lcms2.h>
+
+typedef struct FFIccContext {
+    void *avctx;
+    cmsContext ctx;
+    cmsToneCurve *curves[AVCOL_TRC_NB]; /* tone curve cache */
+} FFIccContext;
+
+/**
+ * Initializes an FFIccContext. This must be done prior to using it.
+ *
+ * Returns 0 on success, or a negative error code.
+ */
+int ff_icc_context_init(FFIccContext *s, void *avctx);
+void ff_icc_context_uninit(FFIccContext *s);
+
+/**
+ * Generate an ICC profile for a given combination of color primaries and
+ * transfer function. Both values must be set to valid entries (not
+ * "undefined") for this function to work.
+ *
+ * Returns 0 on success, or a negative error code.
+ */
+int ff_icc_profile_generate(FFIccContext *s,
+                            enum AVColorPrimaries color_prim,
+                            enum AVColorTransferCharacteristic color_trc,
+                            cmsHPROFILE *out_profile);
+
+/**
+ * Attach an ICC profile to a frame. Helper wrapper around cmsSaveProfileToMem
+ * and av_frame_new_side_data_from_buf.
+ *
+ * Returns 0 on success, or a negative error code.
+ */
+int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame);
+
+/**
+ * Read the color primaries and white point coefficients encoded by an ICC
+ * profile, and return the raw values in `out_primaries`.
+ *
+ * Returns 0 on success, or a negative error code.
+ */
+int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
+                                  AVColorPrimariesDesc *out_primaries);
+
+/**
+ * Attempt detecting the transfer characteristic that best approximates the
+ * transfer function encoded by an ICC profile. Sets `out_trc` to
+ * AVCOL_TRC_UNSPECIFIED if no clear match can be identified.
+ *
+ * Returns 0 on success (including no match), or a negative error code.
+ */
+int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
+                                   enum AVColorTransferCharacteristic *out_trc);
+
+#endif /* AVCODEC_FFLCMS2_H */
diff --git a/libavfilter/fflcms2.c b/libavfilter/fflcms2.c
index fd370fb310..822462de87 100644
--- a/libavfilter/fflcms2.c
+++ b/libavfilter/fflcms2.c
@@ -1,5 +1,4 @@
 /*
- * Copyright (c) 2022 Niklas Haas
  * This file is part of FFmpeg.
  *
  * FFmpeg is free software; you can redistribute it and/or
@@ -17,295 +16,4 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
-#include "libavutil/color_utils.h"
-#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);
-}
-
-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;
-}
+#include "libavcodec/fflcms2.c"
diff --git a/libavfilter/fflcms2.h b/libavfilter/fflcms2.h
index 0d238c679f..1ac29e357b 100644
--- a/libavfilter/fflcms2.h
+++ b/libavfilter/fflcms2.h
@@ -1,5 +1,4 @@
 /*
- * Copyright (c) 2022 Niklas Haas
  * This file is part of FFmpeg.
  *
  * FFmpeg is free software; you can redistribute it and/or
@@ -17,71 +16,9 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
-/**
- * @file
- * Various functions for dealing with ICC profiles
- */
-
 #ifndef AVFILTER_FFLCMS2_H
 #define AVFILTER_FFLCMS2_H
 
-#include "libavutil/csp.h"
-#include "libavutil/frame.h"
-#include "libavutil/pixfmt.h"
-
-#include <lcms2.h>
-
-typedef struct FFIccContext {
-    void *avctx;
-    cmsContext ctx;
-    cmsToneCurve *curves[AVCOL_TRC_NB]; /* tone curve cache */
-} FFIccContext;
-
-/**
- * Initializes an FFIccContext. This must be done prior to using it.
- *
- * Returns 0 on success, or a negative error code.
- */
-int ff_icc_context_init(FFIccContext *s, void *avctx);
-void ff_icc_context_uninit(FFIccContext *s);
-
-/**
- * Generate an ICC profile for a given combination of color primaries and
- * transfer function. Both values must be set to valid entries (not
- * "undefined") for this function to work.
- *
- * Returns 0 on success, or a negative error code.
- */
-int ff_icc_profile_generate(FFIccContext *s,
-                            enum AVColorPrimaries color_prim,
-                            enum AVColorTransferCharacteristic color_trc,
-                            cmsHPROFILE *out_profile);
-
-/**
- * Attach an ICC profile to a frame. Helper wrapper around cmsSaveProfileToMem
- * and av_frame_new_side_data_from_buf.
- *
- * Returns 0 on success, or a negative error code.
- */
-int ff_icc_profile_attach(FFIccContext *s, cmsHPROFILE profile, AVFrame *frame);
-
-/**
- * Read the color primaries and white point coefficients encoded by an ICC
- * profile, and return the raw values in `out_primaries`.
- *
- * Returns 0 on success, or a negative error code.
- */
-int ff_icc_profile_read_primaries(FFIccContext *s, cmsHPROFILE profile,
-                                  AVColorPrimariesDesc *out_primaries);
-
-/**
- * Attempt detecting the transfer characteristic that best approximates the
- * transfer function encoded by an ICC profile. Sets `out_trc` to
- * AVCOL_TRC_UNSPECIFIED if no clear match can be identified.
- *
- * Returns 0 on success (including no match), or a negative error code.
- */
-int ff_icc_profile_detect_transfer(FFIccContext *s, cmsHPROFILE profile,
-                                   enum AVColorTransferCharacteristic *out_trc);
+#include "libavcodec/fflcms2.h"
 
 #endif /* AVFILTER_FFLCMS2_H */
