diff --git a/include/libweston/libweston.h b/include/libweston/libweston.h
index 07df628540e912876d2be703393dbbe736c8bd4a..35fda9163fde7d7400106d5c3e502a2785010ed5 100644
--- a/include/libweston/libweston.h
+++ b/include/libweston/libweston.h
@@ -79,6 +79,7 @@ struct linux_dmabuf_buffer;
 struct weston_recorder;
 struct weston_pointer_constraint;
 struct ro_anonymous_file;
+struct weston_color_profile;
 struct weston_color_transform;
 
 enum weston_keyboard_modifier {
@@ -2125,6 +2126,27 @@ void
 weston_timeline_refresh_subscription_objects(struct weston_compositor *wc,
 					     void *object);
 
+/** Type of color profile object */
+enum weston_color_profile_kind {
+	/** Usable for content. */
+	WESTON_COLOR_PROFILE_KIND_INPUT = 0x01,
+
+	/** Usable for displays. */
+	WESTON_COLOR_PROFILE_KIND_OUTPUT = 0x02,
+
+	/** Usable for both. */
+	WESTON_COLOR_PROFILE_KIND_INPUT_OUTPUT = 0x03, /* INPUT | OUTPUT */
+};
+
+struct weston_color_profile *
+weston_color_profile_ref(struct weston_color_profile *cprof);
+
+void
+weston_color_profile_unref(struct weston_color_profile *cprof);
+
+const char *
+weston_color_profile_get_description(struct weston_color_profile *cprof);
+
 #ifdef  __cplusplus
 }
 #endif
diff --git a/libweston/color-lcms/color-lcms.c b/libweston/color-lcms/color-lcms.c
index e7cff9475d00e6e2e0abb0ba15f06a0076eda2ed..16e998126902885126a504f9271b6c69e4b45fca 100644
--- a/libweston/color-lcms/color-lcms.c
+++ b/libweston/color-lcms/color-lcms.c
@@ -159,6 +159,7 @@ cmlcms_destroy(struct weston_color_manager *cm_base)
 	struct weston_color_manager_lcms *cm = get_cmlcms(cm_base);
 
 	assert(wl_list_empty(&cm->color_transform_list));
+	assert(wl_list_empty(&cm->color_profile_list));
 
 	cmsDeleteContext(cm->lcms_ctx);
 	free(cm);
@@ -178,6 +179,8 @@ weston_color_manager_create(struct weston_compositor *compositor)
 	cm->base.supports_client_protocol = true;
 	cm->base.init = cmlcms_init;
 	cm->base.destroy = cmlcms_destroy;
+	cm->base.destroy_color_profile = cmlcms_destroy_color_profile;
+	cm->base.get_color_profile_from_icc = cmlcms_get_color_profile_from_icc;
 	cm->base.destroy_color_transform = cmlcms_destroy_color_transform;
 	cm->base.get_surface_color_transform =
 	      cmlcms_get_surface_color_transform;
@@ -188,6 +191,7 @@ weston_color_manager_create(struct weston_compositor *compositor)
 	      cmlcms_get_sRGB_to_blend_color_transform;
 
 	wl_list_init(&cm->color_transform_list);
+	wl_list_init(&cm->color_profile_list);
 
 	return &cm->base;
 }
diff --git a/libweston/color-lcms/color-lcms.h b/libweston/color-lcms/color-lcms.h
index 0458d31a22275238b912ef81ad7e482edc9eac69..e7a966cc3deaaaf58679df0af2ff96b62ad1c7c0 100644
--- a/libweston/color-lcms/color-lcms.h
+++ b/libweston/color-lcms/color-lcms.h
@@ -38,6 +38,7 @@ struct weston_color_manager_lcms {
 	cmsContext lcms_ctx;
 
 	struct wl_list color_transform_list; /* cmlcms_color_transform::link */
+	struct wl_list color_profile_list; /* cmlcms_color_profile::link */
 };
 
 static inline struct weston_color_manager_lcms *
@@ -46,6 +47,38 @@ get_cmlcms(struct weston_color_manager *cm_base)
 	return container_of(cm_base, struct weston_color_manager_lcms, base);
 }
 
+struct cmlcms_md5_sum {
+	uint8_t bytes[16];
+};
+
+struct cmlcms_color_profile {
+	struct weston_color_profile base;
+
+	/* struct weston_color_manager_lcms::color_profile_list */
+	struct wl_list link;
+
+	cmsHPROFILE profile;
+	struct cmlcms_md5_sum md5sum;
+};
+
+static inline struct cmlcms_color_profile *
+get_cprof(struct weston_color_profile *cprof_base)
+{
+	return container_of(cprof_base, struct cmlcms_color_profile, base);
+}
+
+bool
+cmlcms_get_color_profile_from_icc(struct weston_color_manager *cm,
+				  const void *icc_data,
+				  size_t icc_len,
+				  enum weston_color_profile_kind usage,
+				  const char *name_part,
+				  struct weston_color_profile **cprof_out,
+				  char **errmsg);
+
+void
+cmlcms_destroy_color_profile(struct weston_color_profile *cprof_base);
+
 /*
  * Perhaps a placeholder, until we get actual color spaces involved and
  * see how this would work better.
diff --git a/libweston/color-lcms/color-profile.c b/libweston/color-lcms/color-profile.c
new file mode 100644
index 0000000000000000000000000000000000000000..085ba346aa2665e6c83ba7440568b353e2a3bfc4
--- /dev/null
+++ b/libweston/color-lcms/color-profile.c
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2019 Sebastian Wick
+ * Copyright 2021 Collabora, Ltd.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice (including the
+ * next paragraph) shall be included in all copies or substantial
+ * portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <libweston/libweston.h>
+
+#include "color.h"
+#include "color-lcms.h"
+#include "shared/helpers.h"
+#include "shared/string-helpers.h"
+
+static bool
+icc_profile_is_valid_for_output(cmsHPROFILE profile, char **errmsg)
+{
+	cmsProfileClassSignature class = cmsGetDeviceClass(profile);
+
+	if (class != cmsSigDisplayClass) {
+		str_printf(errmsg, "output ICC profile is not DisplayClass");
+		return false;
+	}
+
+	return true;
+}
+
+static bool
+icc_profile_is_valid_for_input(cmsHPROFILE profile, char **errmsg)
+{
+	cmsProfileClassSignature class = cmsGetDeviceClass(profile);
+
+	switch (class) {
+	case cmsSigInputClass:
+	case cmsSigOutputClass:
+	case cmsSigAbstractClass:
+	case cmsSigDisplayClass:
+		break;
+	default:
+		str_printf(errmsg, "input ICC profile has bad class");
+		return false;
+	}
+
+	return true;
+}
+
+/* FIXME: sync with spec! */
+static bool
+validate_icc_profile(cmsHPROFILE profile,
+		     enum weston_color_profile_kind *usage,
+		     char **errmsg)
+{
+	cmsColorSpaceSignature cs = cmsGetColorSpace(profile);
+	uint32_t nr_channels = cmsChannelsOf(cs);
+	uint8_t version = cmsGetEncodedICCversion(profile) >> 24;
+	bool ok;
+
+	if (version != 2 && version != 4) {
+		str_printf(errmsg,
+			   "ICC version %d is unsupported, should be 2 or 4.",
+			   version);
+		return false;
+	}
+
+	if (nr_channels != 3) {
+		str_printf(errmsg,
+			   "ICC profile must contain 3 channels for the color space, not %u.\n",
+			   nr_channels);
+		return false;
+	}
+
+	ok = false;
+	switch (*usage) {
+	case WESTON_COLOR_PROFILE_KIND_INPUT:
+		ok = icc_profile_is_valid_for_input(profile, errmsg);
+
+		/*
+		 * Upgrade usage. If the same profile is loaded again as
+		 * INPUT_OUTPUT, it will be found in the cache, and then
+		 * it must also be recorded as INPUT_OUTPUT to begin with.
+		 */
+		if (ok && icc_profile_is_valid_for_output(profile, NULL))
+			*usage = WESTON_COLOR_PROFILE_KIND_INPUT_OUTPUT;
+		break;
+	case WESTON_COLOR_PROFILE_KIND_OUTPUT:
+		ok = icc_profile_is_valid_for_output(profile, errmsg);
+
+		/* Upgrade usage. */
+		if (ok && icc_profile_is_valid_for_input(profile, NULL))
+			*usage = WESTON_COLOR_PROFILE_KIND_INPUT_OUTPUT;
+		break;
+	case WESTON_COLOR_PROFILE_KIND_INPUT_OUTPUT:
+		ok = icc_profile_is_valid_for_input(profile, errmsg) &&
+		     icc_profile_is_valid_for_output(profile, errmsg);
+		break;
+	}
+
+	return ok;
+}
+
+static struct cmlcms_color_profile *
+cmlcms_find_color_profile_by_md5(const struct weston_color_manager_lcms *cm,
+				 const struct cmlcms_md5_sum *md5sum)
+{
+	struct cmlcms_color_profile *cprof;
+
+	wl_list_for_each(cprof, &cm->color_profile_list, link) {
+		if (memcmp(cprof->md5sum.bytes,
+			   md5sum->bytes, sizeof(md5sum->bytes)) == 0)
+			return cprof;
+	}
+
+	return NULL;
+}
+
+static struct cmlcms_color_profile *
+cmlcms_color_profile_create(struct weston_color_manager_lcms *cm,
+			    cmsHPROFILE profile,
+			    enum weston_color_profile_kind usage,
+			    char *desc,
+			    char **errmsg)
+{
+	struct cmlcms_color_profile *cprof;
+
+	cprof = zalloc(sizeof *cprof);
+	if (!cprof)
+		return NULL;
+
+	weston_color_profile_init(&cprof->base, &cm->base, usage);
+	cprof->base.description = desc;
+	cprof->profile = profile;
+	cmsGetHeaderProfileID(profile, cprof->md5sum.bytes);
+	wl_list_insert(&cm->color_profile_list, &cprof->link);
+
+	return cprof;
+}
+
+static void
+cmlcms_color_profile_destroy(struct cmlcms_color_profile *cprof)
+{
+	wl_list_remove(&cprof->link);
+	cmsCloseProfile(cprof->profile);
+	free(cprof->base.description);
+	free(cprof);
+}
+
+static char *
+make_icc_file_description(cmsHPROFILE profile,
+			  const struct cmlcms_md5_sum *md5sum,
+			  const char *name_part)
+{
+	char md5sum_str[sizeof(md5sum->bytes) * 2 + 1];
+	char *desc;
+	size_t i;
+
+	for (i = 0; i < sizeof(md5sum->bytes); i++) {
+		snprintf(md5sum_str + 2 * i, sizeof(md5sum_str) - 2 * i,
+			 "%02x", md5sum->bytes[i]);
+	}
+
+	str_printf(&desc, "ICCv%f %s %s", cmsGetProfileVersion(profile),
+		   name_part, md5sum_str);
+
+	return desc;
+}
+
+bool
+cmlcms_get_color_profile_from_icc(struct weston_color_manager *cm_base,
+				  const void *icc_data,
+				  size_t icc_len,
+				  enum weston_color_profile_kind usage,
+				  const char *name_part,
+				  struct weston_color_profile **cprof_out,
+				  char **errmsg)
+{
+	struct weston_color_manager_lcms *cm = get_cmlcms(cm_base);
+	cmsHPROFILE profile;
+	struct cmlcms_md5_sum md5sum;
+	struct cmlcms_color_profile *cprof;
+	char *desc = NULL;
+
+	if (!icc_data || icc_len < 1) {
+		str_printf(errmsg, "No ICC data.");
+		return false;
+	}
+	if (icc_len >= UINT32_MAX) {
+		str_printf(errmsg, "Too much ICC data.");
+		return false;
+	}
+
+	profile = cmsOpenProfileFromMemTHR(cm->lcms_ctx, icc_data, icc_len);
+	if (!profile) {
+		str_printf(errmsg, "ICC data not understood.");
+		return false;
+	}
+
+	if (!validate_icc_profile(profile, &usage, errmsg))
+		goto err_close;
+
+	if (!cmsMD5computeID(profile)) {
+		str_printf(errmsg, "Failed to compute MD5 for ICC profile.");
+		goto err_close;
+	}
+
+	cmsGetHeaderProfileID(profile, md5sum.bytes);
+	cprof = cmlcms_find_color_profile_by_md5(cm, &md5sum);
+	if (cprof) {
+		*cprof_out = weston_color_profile_ref(&cprof->base);
+		cmsCloseProfile(profile);
+		return true;
+	}
+
+	desc = make_icc_file_description(profile, &md5sum, name_part);
+	if (!desc)
+		goto err_close;
+
+	cprof = cmlcms_color_profile_create(cm, profile, usage, desc, errmsg);
+	if (!cprof)
+		goto err_close;
+
+	*cprof_out = &cprof->base;
+	return true;
+
+err_close:
+	free(desc);
+	cmsCloseProfile(profile);
+	return false;
+}
+
+void
+cmlcms_destroy_color_profile(struct weston_color_profile *cprof_base)
+{
+	struct cmlcms_color_profile *cprof = get_cprof(cprof_base);
+
+	cmlcms_color_profile_destroy(cprof);
+}
diff --git a/libweston/color-lcms/meson.build b/libweston/color-lcms/meson.build
index 1f11013f8e9096dfcf52d825a472b1e9eeda9b3d..86e2871f7d19391198c3bc7fbebf7eee78066eb9 100644
--- a/libweston/color-lcms/meson.build
+++ b/libweston/color-lcms/meson.build
@@ -9,6 +9,7 @@ endif
 
 srcs_color_lcms = [
 	'color-lcms.c',
+	'color-profile.c',
 	'color-transform.c',
 ]
 
diff --git a/libweston/color-noop.c b/libweston/color-noop.c
index aa73d42e54c343c1023b69f9e698654d8f6e0828..557797dcec08b244f7a53a47602f15f2a7f79570 100644
--- a/libweston/color-noop.c
+++ b/libweston/color-noop.c
@@ -29,6 +29,7 @@
 
 #include "color.h"
 #include "shared/helpers.h"
+#include "shared/string-helpers.h"
 
 struct weston_color_manager_noop {
 	struct weston_color_manager base;
@@ -40,6 +41,25 @@ get_cmnoop(struct weston_color_manager *cm_base)
 	return container_of(cm_base, struct weston_color_manager_noop, base);
 }
 
+static void
+cmnoop_destroy_color_profile(struct weston_color_profile *cprof)
+{
+	/* Never called, as never creates an actual color profile. */
+}
+
+static bool
+cmnoop_get_color_profile_from_icc(struct weston_color_manager *cm,
+				  const void *icc_data,
+				  size_t icc_len,
+				  enum weston_color_profile_kind usage,
+				  const char *name_part,
+				  struct weston_color_profile **cprof_out,
+				  char **errmsg)
+{
+	str_printf(errmsg, "ICC profiles are unsupported.");
+	return false;
+}
+
 static void
 cmnoop_destroy_color_transform(struct weston_color_transform *xform)
 {
@@ -131,6 +151,8 @@ weston_color_manager_noop_create(struct weston_compositor *compositor)
 	cm->base.supports_client_protocol = false;
 	cm->base.init = cmnoop_init;
 	cm->base.destroy = cmnoop_destroy;
+	cm->base.destroy_color_profile = cmnoop_destroy_color_profile;
+	cm->base.get_color_profile_from_icc = cmnoop_get_color_profile_from_icc;
 	cm->base.destroy_color_transform = cmnoop_destroy_color_transform;
 	cm->base.get_surface_color_transform =
 	      cmnoop_get_surface_color_transform;
diff --git a/libweston/color.c b/libweston/color.c
index 191b02fe9a3dcdd613df8fd38537fab7948e7cae..455a19e088aaccb5b74be62cedd5f3ba00f8acd4 100644
--- a/libweston/color.c
+++ b/libweston/color.c
@@ -31,6 +31,79 @@
 #include "color.h"
 #include "libweston-internal.h"
 
+/**
+ * Increase reference count of the color profile object
+ *
+ * \param cprof The color profile. NULL is accepted too.
+ * \return cprof.
+ */
+WL_EXPORT struct weston_color_profile *
+weston_color_profile_ref(struct weston_color_profile *cprof)
+{
+	/* NULL is a valid color space: sRGB */
+	if (!cprof)
+		return NULL;
+
+	assert(cprof->ref_count > 0);
+	cprof->ref_count++;
+	return cprof;
+}
+
+/**
+ * Decrease reference count and potentially destroy the color profile object
+ *
+ * \param cprof The color profile. NULL is accepted too.
+ */
+WL_EXPORT void
+weston_color_profile_unref(struct weston_color_profile *cprof)
+{
+	if (!cprof)
+		return;
+
+	assert(cprof->ref_count > 0);
+	if (--cprof->ref_count > 0)
+		return;
+
+	cprof->cm->destroy_color_profile(cprof);
+}
+
+/**
+ * Get color profile description
+ *
+ * A description of the profile is meant for human readable logs.
+ *
+ * \param cprof The color profile, NULL is accepted too.
+ * \returns The color profile description, valid as long as the
+ * color profile itself is.
+ */
+WL_EXPORT const char *
+weston_color_profile_get_description(struct weston_color_profile *cprof)
+{
+	if (cprof)
+		return cprof->description;
+	else
+		return "built-in default sRGB SDR profile";
+}
+
+/**
+ * Initializes a newly allocated color profile object
+ *
+ * This is used only by color managers. They sub-class weston_color_profile.
+ *
+ * The reference count starts at 1.
+ *
+ * To destroy a weston_color_profile, use weston_color_profile_unref().
+ */
+WL_EXPORT void
+weston_color_profile_init(struct weston_color_profile *cprof,
+			  struct weston_color_manager *cm,
+			  enum weston_color_profile_kind usage)
+{
+	cprof->cm = cm;
+	cprof->ref_count = 1;
+	cprof->usage = usage;
+}
+
 /**
  * Increase reference count of the color transform object
  *
diff --git a/libweston/color.h b/libweston/color.h
index 30a839242c8b8f62f759b0fe37592ebfd598654f..34669dfbf1fca6cd34bf8cada27e6aab03933bc0 100644
--- a/libweston/color.h
+++ b/libweston/color.h
@@ -27,8 +27,21 @@
 #define WESTON_COLOR_H
 
 #include <stdbool.h>
+#include <stdint.h>
 #include <libweston/libweston.h>
 
+/**
+ * Represents a color profile description (an ICC color profile)
+ *
+ * Sub-classed by the color manager that created this.
+ */
+struct weston_color_profile {
+	struct weston_color_manager *cm;
+	int ref_count;
+	enum weston_color_profile_kind usage;
+	char *description;
+};
+
 /** Type or formula for a curve */
 enum weston_color_curve_type {
 	/** Identity function, no-op */
@@ -161,6 +174,37 @@ struct weston_color_manager {
 	void
 	(*destroy)(struct weston_color_manager *cm);
 
+	/** Destroy a color profile after refcount fell to zero */
+	void
+	(*destroy_color_profile)(struct weston_color_profile *cprof);
+
+	/** Create a color profile from ICC data
+	 *
+	 * \param cm The color manager.
+	 * \param icc_data Pointer to the ICC binary data.
+	 * \param icc_len Length of the ICC data in bytes.
+	 * \param usage What usage this profile must at least support.
+	 * \param name_part A string to be used in describing the profile.
+	 * \param cprof_out On success, the created object is returned here.
+	 * On failure, untouched.
+	 * \param errmsg On success, untouched. On failure, a pointer to a
+	 * string describing the error is stored here. The string must be
+	 * free()'d.
+	 * \return True on success, false on failure.
+	 *
+	 * This may return a new reference to an existing color profile if
+	 * that profile is identical to the one that would be created, apart
+	 * from name_part.
+	 */
+	bool
+	(*get_color_profile_from_icc)(struct weston_color_manager *cm,
+				      const void *icc_data,
+				      size_t icc_len,
+				      enum weston_color_profile_kind usage,
+				      const char *name_part,
+				      struct weston_color_profile **cprof_out,
+				      char **errmsg);
+
 	/** Destroy a color transform after refcount fell to zero */
 	void
 	(*destroy_color_transform)(struct weston_color_transform *xform);
@@ -231,6 +275,11 @@ struct weston_color_manager {
 					     struct weston_color_transform **xform_out);
 };
 
+void
+weston_color_profile_init(struct weston_color_profile *cprof,
+			  struct weston_color_manager *cm,
+			  enum weston_color_profile_kind usage);
+
 struct weston_color_transform *
 weston_color_transform_ref(struct weston_color_transform *xform);