summaryrefslogtreecommitdiffstats
path: root/src/colorspace.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/colorspace.c')
-rw-r--r--src/colorspace.c1609
1 files changed, 1609 insertions, 0 deletions
diff --git a/src/colorspace.c b/src/colorspace.c
new file mode 100644
index 0000000..5cef2b5
--- /dev/null
+++ b/src/colorspace.c
@@ -0,0 +1,1609 @@
+/*
+ * This file is part of libplacebo.
+ *
+ * libplacebo 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.
+ *
+ * libplacebo 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 libplacebo. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <math.h>
+
+#include "common.h"
+#include "hash.h"
+
+#include <libplacebo/colorspace.h>
+#include <libplacebo/tone_mapping.h>
+
+bool pl_color_system_is_ycbcr_like(enum pl_color_system sys)
+{
+ switch (sys) {
+ case PL_COLOR_SYSTEM_UNKNOWN:
+ case PL_COLOR_SYSTEM_RGB:
+ case PL_COLOR_SYSTEM_XYZ:
+ return false;
+ case PL_COLOR_SYSTEM_BT_601:
+ case PL_COLOR_SYSTEM_BT_709:
+ case PL_COLOR_SYSTEM_SMPTE_240M:
+ case PL_COLOR_SYSTEM_BT_2020_NC:
+ case PL_COLOR_SYSTEM_BT_2020_C:
+ case PL_COLOR_SYSTEM_BT_2100_PQ:
+ case PL_COLOR_SYSTEM_BT_2100_HLG:
+ case PL_COLOR_SYSTEM_DOLBYVISION:
+ case PL_COLOR_SYSTEM_YCGCO:
+ return true;
+ case PL_COLOR_SYSTEM_COUNT: break;
+ };
+
+ pl_unreachable();
+}
+
+bool pl_color_system_is_linear(enum pl_color_system sys)
+{
+ switch (sys) {
+ case PL_COLOR_SYSTEM_UNKNOWN:
+ case PL_COLOR_SYSTEM_RGB:
+ case PL_COLOR_SYSTEM_BT_601:
+ case PL_COLOR_SYSTEM_BT_709:
+ case PL_COLOR_SYSTEM_SMPTE_240M:
+ case PL_COLOR_SYSTEM_BT_2020_NC:
+ case PL_COLOR_SYSTEM_YCGCO:
+ return true;
+ case PL_COLOR_SYSTEM_BT_2020_C:
+ case PL_COLOR_SYSTEM_BT_2100_PQ:
+ case PL_COLOR_SYSTEM_BT_2100_HLG:
+ case PL_COLOR_SYSTEM_DOLBYVISION:
+ case PL_COLOR_SYSTEM_XYZ:
+ return false;
+ case PL_COLOR_SYSTEM_COUNT: break;
+ };
+
+ pl_unreachable();
+}
+
+enum pl_color_system pl_color_system_guess_ycbcr(int width, int height)
+{
+ if (width >= 1280 || height > 576) {
+ // Typical HD content
+ return PL_COLOR_SYSTEM_BT_709;
+ } else {
+ // Typical SD content
+ return PL_COLOR_SYSTEM_BT_601;
+ }
+}
+
+bool pl_bit_encoding_equal(const struct pl_bit_encoding *b1,
+ const struct pl_bit_encoding *b2)
+{
+ return b1->sample_depth == b2->sample_depth &&
+ b1->color_depth == b2->color_depth &&
+ b1->bit_shift == b2->bit_shift;
+}
+
+const struct pl_color_repr pl_color_repr_unknown = {0};
+
+const struct pl_color_repr pl_color_repr_rgb = {
+ .sys = PL_COLOR_SYSTEM_RGB,
+ .levels = PL_COLOR_LEVELS_FULL,
+};
+
+const struct pl_color_repr pl_color_repr_sdtv = {
+ .sys = PL_COLOR_SYSTEM_BT_601,
+ .levels = PL_COLOR_LEVELS_LIMITED,
+};
+
+const struct pl_color_repr pl_color_repr_hdtv = {
+ .sys = PL_COLOR_SYSTEM_BT_709,
+ .levels = PL_COLOR_LEVELS_LIMITED,
+};
+
+const struct pl_color_repr pl_color_repr_uhdtv = {
+ .sys = PL_COLOR_SYSTEM_BT_2020_NC,
+ .levels = PL_COLOR_LEVELS_LIMITED,
+};
+
+const struct pl_color_repr pl_color_repr_jpeg = {
+ .sys = PL_COLOR_SYSTEM_BT_601,
+ .levels = PL_COLOR_LEVELS_FULL,
+};
+
+bool pl_color_repr_equal(const struct pl_color_repr *c1,
+ const struct pl_color_repr *c2)
+{
+ return c1->sys == c2->sys &&
+ c1->levels == c2->levels &&
+ c1->alpha == c2->alpha &&
+ c1->dovi == c2->dovi &&
+ pl_bit_encoding_equal(&c1->bits, &c2->bits);
+}
+
+static struct pl_bit_encoding pl_bit_encoding_merge(const struct pl_bit_encoding *orig,
+ const struct pl_bit_encoding *new)
+{
+ return (struct pl_bit_encoding) {
+ .sample_depth = PL_DEF(orig->sample_depth, new->sample_depth),
+ .color_depth = PL_DEF(orig->color_depth, new->color_depth),
+ .bit_shift = PL_DEF(orig->bit_shift, new->bit_shift),
+ };
+}
+
+void pl_color_repr_merge(struct pl_color_repr *orig, const struct pl_color_repr *new)
+{
+ *orig = (struct pl_color_repr) {
+ .sys = PL_DEF(orig->sys, new->sys),
+ .levels = PL_DEF(orig->levels, new->levels),
+ .alpha = PL_DEF(orig->alpha, new->alpha),
+ .dovi = PL_DEF(orig->dovi, new->dovi),
+ .bits = pl_bit_encoding_merge(&orig->bits, &new->bits),
+ };
+}
+
+enum pl_color_levels pl_color_levels_guess(const struct pl_color_repr *repr)
+{
+ if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION)
+ return PL_COLOR_LEVELS_FULL;
+
+ if (repr->levels)
+ return repr->levels;
+
+ return pl_color_system_is_ycbcr_like(repr->sys)
+ ? PL_COLOR_LEVELS_LIMITED
+ : PL_COLOR_LEVELS_FULL;
+}
+
+float pl_color_repr_normalize(struct pl_color_repr *repr)
+{
+ float scale = 1.0;
+ struct pl_bit_encoding *bits = &repr->bits;
+
+ if (bits->bit_shift) {
+ scale /= (1LL << bits->bit_shift);
+ bits->bit_shift = 0;
+ }
+
+ // If one of these is set but not the other, use the set one
+ int tex_bits = PL_DEF(bits->sample_depth, 8);
+ int col_bits = PL_DEF(bits->color_depth, tex_bits);
+ tex_bits = PL_DEF(tex_bits, col_bits);
+
+ if (pl_color_levels_guess(repr) == PL_COLOR_LEVELS_LIMITED) {
+ // Limit range is always shifted directly
+ scale *= (float) (1LL << tex_bits) / (1LL << col_bits);
+ } else {
+ // Full range always uses the full range available
+ scale *= ((1LL << tex_bits) - 1.) / ((1LL << col_bits) - 1.);
+ }
+
+ bits->color_depth = bits->sample_depth;
+ return scale;
+}
+
+bool pl_color_primaries_is_wide_gamut(enum pl_color_primaries prim)
+{
+ switch (prim) {
+ case PL_COLOR_PRIM_UNKNOWN:
+ case PL_COLOR_PRIM_BT_601_525:
+ case PL_COLOR_PRIM_BT_601_625:
+ case PL_COLOR_PRIM_BT_709:
+ case PL_COLOR_PRIM_BT_470M:
+ case PL_COLOR_PRIM_EBU_3213:
+ return false;
+ case PL_COLOR_PRIM_BT_2020:
+ case PL_COLOR_PRIM_APPLE:
+ case PL_COLOR_PRIM_ADOBE:
+ case PL_COLOR_PRIM_PRO_PHOTO:
+ case PL_COLOR_PRIM_CIE_1931:
+ case PL_COLOR_PRIM_DCI_P3:
+ case PL_COLOR_PRIM_DISPLAY_P3:
+ case PL_COLOR_PRIM_V_GAMUT:
+ case PL_COLOR_PRIM_S_GAMUT:
+ case PL_COLOR_PRIM_FILM_C:
+ case PL_COLOR_PRIM_ACES_AP0:
+ case PL_COLOR_PRIM_ACES_AP1:
+ return true;
+ case PL_COLOR_PRIM_COUNT: break;
+ }
+
+ pl_unreachable();
+}
+
+enum pl_color_primaries pl_color_primaries_guess(int width, int height)
+{
+ // HD content
+ if (width >= 1280 || height > 576)
+ return PL_COLOR_PRIM_BT_709;
+
+ switch (height) {
+ case 576: // Typical PAL content, including anamorphic/squared
+ return PL_COLOR_PRIM_BT_601_625;
+
+ case 480: // Typical NTSC content, including squared
+ case 486: // NTSC Pro or anamorphic NTSC
+ return PL_COLOR_PRIM_BT_601_525;
+
+ default: // No good metric, just pick BT.709 to minimize damage
+ return PL_COLOR_PRIM_BT_709;
+ }
+}
+
+// HLG 75% value (scene-referred)
+#define HLG_75 3.17955
+
+float pl_color_transfer_nominal_peak(enum pl_color_transfer trc)
+{
+ switch (trc) {
+ case PL_COLOR_TRC_UNKNOWN:
+ case PL_COLOR_TRC_BT_1886:
+ case PL_COLOR_TRC_SRGB:
+ case PL_COLOR_TRC_LINEAR:
+ case PL_COLOR_TRC_GAMMA18:
+ case PL_COLOR_TRC_GAMMA20:
+ case PL_COLOR_TRC_GAMMA22:
+ case PL_COLOR_TRC_GAMMA24:
+ case PL_COLOR_TRC_GAMMA26:
+ case PL_COLOR_TRC_GAMMA28:
+ case PL_COLOR_TRC_PRO_PHOTO:
+ case PL_COLOR_TRC_ST428:
+ return 1.0;
+ case PL_COLOR_TRC_PQ: return 10000.0 / PL_COLOR_SDR_WHITE;
+ case PL_COLOR_TRC_HLG: return 12.0 / HLG_75;
+ case PL_COLOR_TRC_V_LOG: return 46.0855;
+ case PL_COLOR_TRC_S_LOG1: return 6.52;
+ case PL_COLOR_TRC_S_LOG2: return 9.212;
+ case PL_COLOR_TRC_COUNT: break;
+ }
+
+ pl_unreachable();
+}
+
+const struct pl_hdr_metadata pl_hdr_metadata_empty = {0};
+const struct pl_hdr_metadata pl_hdr_metadata_hdr10 ={
+ .prim = {
+ .red = {0.708, 0.292},
+ .green = {0.170, 0.797},
+ .blue = {0.131, 0.046},
+ .white = {0.31271, 0.32902},
+ },
+ .min_luma = 0,
+ .max_luma = 10000,
+ .max_cll = 10000,
+ .max_fall = 0, // unknown
+};
+
+static const float PQ_M1 = 2610./4096 * 1./4,
+ PQ_M2 = 2523./4096 * 128,
+ PQ_C1 = 3424./4096,
+ PQ_C2 = 2413./4096 * 32,
+ PQ_C3 = 2392./4096 * 32;
+
+float pl_hdr_rescale(enum pl_hdr_scaling from, enum pl_hdr_scaling to, float x)
+{
+ if (from == to)
+ return x;
+ if (!x) // micro-optimization for common value
+ return x;
+
+ x = fmaxf(x, 0.0f);
+
+ // Convert input to PL_SCALE_RELATIVE
+ switch (from) {
+ case PL_HDR_PQ:
+ x = powf(x, 1.0f / PQ_M2);
+ x = fmaxf(x - PQ_C1, 0.0f) / (PQ_C2 - PQ_C3 * x);
+ x = powf(x, 1.0f / PQ_M1);
+ x *= 10000.0f;
+ // fall through
+ case PL_HDR_NITS:
+ x /= PL_COLOR_SDR_WHITE;
+ // fall through
+ case PL_HDR_NORM:
+ goto output;
+ case PL_HDR_SQRT:
+ x *= x;
+ goto output;
+ case PL_HDR_SCALING_COUNT:
+ break;
+ }
+
+ pl_unreachable();
+
+output:
+ // Convert PL_SCALE_RELATIVE to output
+ switch (to) {
+ case PL_HDR_NORM:
+ return x;
+ case PL_HDR_SQRT:
+ return sqrtf(x);
+ case PL_HDR_NITS:
+ return x * PL_COLOR_SDR_WHITE;
+ case PL_HDR_PQ:
+ x *= PL_COLOR_SDR_WHITE / 10000.0f;
+ x = powf(x, PQ_M1);
+ x = (PQ_C1 + PQ_C2 * x) / (1.0f + PQ_C3 * x);
+ x = powf(x, PQ_M2);
+ return x;
+ case PL_HDR_SCALING_COUNT:
+ break;
+ }
+
+ pl_unreachable();
+}
+
+static inline bool pl_hdr_bezier_equal(const struct pl_hdr_bezier *a,
+ const struct pl_hdr_bezier *b)
+{
+ return a->target_luma == b->target_luma &&
+ a->knee_x == b->knee_x &&
+ a->knee_y == b->knee_y &&
+ a->num_anchors == b->num_anchors &&
+ !memcmp(a->anchors, b->anchors, sizeof(a->anchors[0]) * a->num_anchors);
+}
+
+bool pl_hdr_metadata_equal(const struct pl_hdr_metadata *a,
+ const struct pl_hdr_metadata *b)
+{
+ return pl_raw_primaries_equal(&a->prim, &b->prim) &&
+ a->min_luma == b->min_luma &&
+ a->max_luma == b->max_luma &&
+ a->max_cll == b->max_cll &&
+ a->max_fall == b->max_fall &&
+ a->scene_max[0] == b->scene_max[0] &&
+ a->scene_max[1] == b->scene_max[1] &&
+ a->scene_max[2] == b->scene_max[2] &&
+ a->scene_avg == b->scene_avg &&
+ pl_hdr_bezier_equal(&a->ootf, &b->ootf) &&
+ a->max_pq_y == b->max_pq_y &&
+ a->avg_pq_y == b->avg_pq_y;
+}
+
+void pl_hdr_metadata_merge(struct pl_hdr_metadata *orig,
+ const struct pl_hdr_metadata *update)
+{
+ pl_raw_primaries_merge(&orig->prim, &update->prim);
+ if (!orig->min_luma)
+ orig->min_luma = update->min_luma;
+ if (!orig->max_luma)
+ orig->max_luma = update->max_luma;
+ if (!orig->max_cll)
+ orig->max_cll = update->max_cll;
+ if (!orig->max_fall)
+ orig->max_fall = update->max_fall;
+ if (!orig->scene_max[1])
+ memcpy(orig->scene_max, update->scene_max, sizeof(orig->scene_max));
+ if (!orig->scene_avg)
+ orig->scene_avg = update->scene_avg;
+ if (!orig->ootf.target_luma)
+ orig->ootf = update->ootf;
+ if (!orig->max_pq_y)
+ orig->max_pq_y = update->max_pq_y;
+ if (!orig->avg_pq_y)
+ orig->avg_pq_y = update->avg_pq_y;
+}
+
+bool pl_hdr_metadata_contains(const struct pl_hdr_metadata *data,
+ enum pl_hdr_metadata_type type)
+{
+ bool has_hdr10 = data->max_luma;
+ bool has_hdr10plus = data->scene_avg && (data->scene_max[0] ||
+ data->scene_max[1] ||
+ data->scene_max[2]);
+ bool has_cie_y = data->max_pq_y && data->avg_pq_y;
+
+ switch (type) {
+ case PL_HDR_METADATA_NONE: return true;
+ case PL_HDR_METADATA_ANY: return has_hdr10 || has_hdr10plus || has_cie_y;
+ case PL_HDR_METADATA_HDR10: return has_hdr10;
+ case PL_HDR_METADATA_HDR10PLUS: return has_hdr10plus;
+ case PL_HDR_METADATA_CIE_Y: return has_cie_y;
+ case PL_HDR_METADATA_TYPE_COUNT: break;
+ }
+
+ pl_unreachable();
+}
+
+const struct pl_color_space pl_color_space_unknown = {0};
+
+const struct pl_color_space pl_color_space_srgb = {
+ .primaries = PL_COLOR_PRIM_BT_709,
+ .transfer = PL_COLOR_TRC_SRGB,
+};
+
+const struct pl_color_space pl_color_space_bt709 = {
+ .primaries = PL_COLOR_PRIM_BT_709,
+ .transfer = PL_COLOR_TRC_BT_1886,
+};
+
+const struct pl_color_space pl_color_space_hdr10 = {
+ .primaries = PL_COLOR_PRIM_BT_2020,
+ .transfer = PL_COLOR_TRC_PQ,
+};
+
+const struct pl_color_space pl_color_space_bt2020_hlg = {
+ .primaries = PL_COLOR_PRIM_BT_2020,
+ .transfer = PL_COLOR_TRC_HLG,
+};
+
+const struct pl_color_space pl_color_space_monitor = {
+ .primaries = PL_COLOR_PRIM_BT_709, // sRGB primaries
+ .transfer = PL_COLOR_TRC_UNKNOWN, // unknown SDR response
+};
+
+bool pl_color_space_is_hdr(const struct pl_color_space *csp)
+{
+ return csp->hdr.max_luma > PL_COLOR_SDR_WHITE ||
+ pl_color_transfer_is_hdr(csp->transfer);
+}
+
+bool pl_color_space_is_black_scaled(const struct pl_color_space *csp)
+{
+ switch (csp->transfer) {
+ case PL_COLOR_TRC_UNKNOWN:
+ case PL_COLOR_TRC_SRGB:
+ case PL_COLOR_TRC_LINEAR:
+ case PL_COLOR_TRC_GAMMA18:
+ case PL_COLOR_TRC_GAMMA20:
+ case PL_COLOR_TRC_GAMMA22:
+ case PL_COLOR_TRC_GAMMA24:
+ case PL_COLOR_TRC_GAMMA26:
+ case PL_COLOR_TRC_GAMMA28:
+ case PL_COLOR_TRC_PRO_PHOTO:
+ case PL_COLOR_TRC_ST428:
+ case PL_COLOR_TRC_HLG:
+ return true;
+
+ case PL_COLOR_TRC_BT_1886:
+ case PL_COLOR_TRC_PQ:
+ case PL_COLOR_TRC_V_LOG:
+ case PL_COLOR_TRC_S_LOG1:
+ case PL_COLOR_TRC_S_LOG2:
+ return false;
+
+ case PL_COLOR_TRC_COUNT: break;
+ }
+
+ pl_unreachable();
+}
+
+void pl_color_space_merge(struct pl_color_space *orig,
+ const struct pl_color_space *new)
+{
+ if (!orig->primaries)
+ orig->primaries = new->primaries;
+ if (!orig->transfer)
+ orig->transfer = new->transfer;
+ pl_hdr_metadata_merge(&orig->hdr, &new->hdr);
+}
+
+bool pl_color_space_equal(const struct pl_color_space *c1,
+ const struct pl_color_space *c2)
+{
+ return c1->primaries == c2->primaries &&
+ c1->transfer == c2->transfer &&
+ pl_hdr_metadata_equal(&c1->hdr, &c2->hdr);
+}
+
+// Estimates luminance from maxRGB by looking at how monochromatic MaxSCL is
+static void luma_from_maxrgb(const struct pl_color_space *csp,
+ enum pl_hdr_scaling scaling,
+ float *out_max, float *out_avg)
+{
+ const float maxscl = PL_MAX3(csp->hdr.scene_max[0],
+ csp->hdr.scene_max[1],
+ csp->hdr.scene_max[2]);
+ if (!maxscl)
+ return;
+
+ struct pl_raw_primaries prim = csp->hdr.prim;
+ pl_raw_primaries_merge(&prim, pl_raw_primaries_get(csp->primaries));
+ const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&prim);
+
+ const float max_luma = rgb2xyz.m[1][0] * csp->hdr.scene_max[0] +
+ rgb2xyz.m[1][1] * csp->hdr.scene_max[1] +
+ rgb2xyz.m[1][2] * csp->hdr.scene_max[2];
+
+ const float coef = max_luma / maxscl;
+ *out_max = pl_hdr_rescale(PL_HDR_NITS, scaling, max_luma);
+ *out_avg = pl_hdr_rescale(PL_HDR_NITS, scaling, coef * csp->hdr.scene_avg);
+}
+
+static inline bool metadata_compat(enum pl_hdr_metadata_type metadata,
+ enum pl_hdr_metadata_type compat)
+{
+ return metadata == PL_HDR_METADATA_ANY || metadata == compat;
+}
+
+void pl_color_space_nominal_luma_ex(const struct pl_nominal_luma_params *params)
+{
+ if (!params || (!params->out_min && !params->out_max && !params->out_avg))
+ return;
+
+ const struct pl_color_space *csp = params->color;
+ const enum pl_hdr_scaling scaling = params->scaling;
+
+ float min_luma = 0, max_luma = 0, avg_luma = 0;
+ if (params->metadata != PL_HDR_METADATA_NONE) {
+ // Initialize from static HDR10 metadata, in all cases
+ min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.min_luma);
+ max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.max_luma);
+ }
+
+ if (metadata_compat(params->metadata, PL_HDR_METADATA_HDR10PLUS) &&
+ pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_HDR10PLUS))
+ {
+ luma_from_maxrgb(csp, scaling, &max_luma, &avg_luma);
+ }
+
+ if (metadata_compat(params->metadata, PL_HDR_METADATA_CIE_Y) &&
+ pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_CIE_Y))
+ {
+ max_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.max_pq_y);
+ avg_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.avg_pq_y);
+ }
+
+ // Clamp to sane value range
+ const float hdr_min = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HDR_BLACK);
+ const float hdr_max = pl_hdr_rescale(PL_HDR_PQ, scaling, 1.0f);
+ max_luma = max_luma ? PL_CLAMP(max_luma, hdr_min, hdr_max) : 0;
+ min_luma = min_luma ? PL_CLAMP(min_luma, hdr_min, hdr_max) : 0;
+ if ((max_luma && min_luma >= max_luma) || min_luma >= hdr_max)
+ min_luma = max_luma = 0; // sanity
+
+ // PQ is always scaled down to absolute black, ignoring HDR metadata
+ if (csp->transfer == PL_COLOR_TRC_PQ)
+ min_luma = hdr_min;
+
+ // Baseline/fallback metadata, inferred entirely from the colorspace
+ // description and built-in default assumptions
+ if (!max_luma) {
+ if (csp->transfer == PL_COLOR_TRC_HLG) {
+ max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HLG_PEAK);
+ } else {
+ const float peak = pl_color_transfer_nominal_peak(csp->transfer);
+ max_luma = pl_hdr_rescale(PL_HDR_NORM, scaling, peak);
+ }
+ }
+
+ if (!min_luma) {
+ if (pl_color_transfer_is_hdr(csp->transfer)) {
+ min_luma = hdr_min;
+ } else {
+ const float peak = pl_hdr_rescale(scaling, PL_HDR_NITS, max_luma);
+ min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling,
+ peak / PL_COLOR_SDR_CONTRAST);
+ }
+ }
+
+ if (avg_luma)
+ avg_luma = PL_CLAMP(avg_luma, min_luma, max_luma); // sanity
+
+ if (params->out_min)
+ *params->out_min = min_luma;
+ if (params->out_max)
+ *params->out_max = max_luma;
+ if (params->out_avg)
+ *params->out_avg = avg_luma;
+}
+
+void pl_color_space_nominal_luma(const struct pl_color_space *csp,
+ float *out_min, float *out_max)
+{
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .color = csp,
+ .metadata = PL_HDR_METADATA_ANY,
+ .scaling = PL_HDR_NORM,
+ .out_min = out_min,
+ .out_max = out_max,
+ ));
+}
+
+void pl_color_space_infer(struct pl_color_space *space)
+{
+ if (!space->primaries)
+ space->primaries = PL_COLOR_PRIM_BT_709;
+ if (!space->transfer)
+ space->transfer = PL_COLOR_TRC_BT_1886;
+
+ // Sanitize the static HDR metadata
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .color = space,
+ .metadata = PL_HDR_METADATA_HDR10,
+ .scaling = PL_HDR_NITS,
+ .out_max = &space->hdr.max_luma,
+ // Preserve tagged minimum
+ .out_min = space->hdr.min_luma ? NULL : &space->hdr.min_luma,
+ ));
+
+ // Default the signal color space based on the nominal raw primaries
+ if (!pl_primaries_valid(&space->hdr.prim))
+ space->hdr.prim = *pl_raw_primaries_get(space->primaries);
+}
+
+static void infer_both_ref(struct pl_color_space *space,
+ struct pl_color_space *ref)
+{
+ pl_color_space_infer(ref);
+
+ if (!space->primaries) {
+ if (pl_color_primaries_is_wide_gamut(ref->primaries)) {
+ space->primaries = PL_COLOR_PRIM_BT_709;
+ } else {
+ space->primaries = ref->primaries;
+ }
+ }
+
+ if (!space->transfer) {
+ switch (ref->transfer) {
+ case PL_COLOR_TRC_UNKNOWN:
+ case PL_COLOR_TRC_COUNT:
+ pl_unreachable();
+ case PL_COLOR_TRC_BT_1886:
+ case PL_COLOR_TRC_SRGB:
+ case PL_COLOR_TRC_GAMMA22:
+ // Re-use input transfer curve to avoid small adaptations
+ space->transfer = ref->transfer;
+ break;
+ case PL_COLOR_TRC_PQ:
+ case PL_COLOR_TRC_HLG:
+ case PL_COLOR_TRC_V_LOG:
+ case PL_COLOR_TRC_S_LOG1:
+ case PL_COLOR_TRC_S_LOG2:
+ // Pick BT.1886 model because it models SDR contrast accurately,
+ // and we need contrast information for tone mapping
+ space->transfer = PL_COLOR_TRC_BT_1886;
+ break;
+ case PL_COLOR_TRC_PRO_PHOTO:
+ // ProPhotoRGB and sRGB are both piecewise with linear slope
+ space->transfer = PL_COLOR_TRC_SRGB;
+ break;
+ case PL_COLOR_TRC_LINEAR:
+ case PL_COLOR_TRC_GAMMA18:
+ case PL_COLOR_TRC_GAMMA20:
+ case PL_COLOR_TRC_GAMMA24:
+ case PL_COLOR_TRC_GAMMA26:
+ case PL_COLOR_TRC_GAMMA28:
+ case PL_COLOR_TRC_ST428:
+ // Pick pure power output curve to avoid introducing black crush
+ space->transfer = PL_COLOR_TRC_GAMMA22;
+ break;
+ }
+ }
+
+ // Infer the remaining fields after making the above choices
+ pl_color_space_infer(space);
+}
+
+void pl_color_space_infer_ref(struct pl_color_space *space,
+ const struct pl_color_space *refp)
+{
+ // Make a copy of `refp` to infer missing values first
+ struct pl_color_space ref = *refp;
+ infer_both_ref(space, &ref);
+}
+
+void pl_color_space_infer_map(struct pl_color_space *src,
+ struct pl_color_space *dst)
+{
+ bool unknown_src_contrast = !src->hdr.min_luma;
+ bool unknown_dst_contrast = !dst->hdr.min_luma;
+
+ infer_both_ref(dst, src);
+
+ // If the src has an unspecified gamma curve with dynamic black scaling,
+ // default it to match the dst colorspace contrast. This does not matter in
+ // most cases, but ensures that BT.1886 is tuned to the appropriate black
+ // point by default.
+ bool dynamic_src_contrast = pl_color_space_is_black_scaled(src) ||
+ src->transfer == PL_COLOR_TRC_BT_1886;
+ if (unknown_src_contrast && dynamic_src_contrast)
+ src->hdr.min_luma = dst->hdr.min_luma;
+
+ // Do the same in reverse if both src and dst are SDR curves
+ bool src_is_sdr = !pl_color_space_is_hdr(src);
+ bool dst_is_sdr = !pl_color_space_is_hdr(dst);
+ if (unknown_dst_contrast && src_is_sdr && dst_is_sdr)
+ dst->hdr.min_luma = src->hdr.min_luma;
+
+ // If the src is HLG and the output is HDR, tune the HLG peak to the output
+ if (src->transfer == PL_COLOR_TRC_HLG && pl_color_space_is_hdr(dst))
+ src->hdr.max_luma = dst->hdr.max_luma;
+}
+
+const struct pl_color_adjustment pl_color_adjustment_neutral = {
+ PL_COLOR_ADJUSTMENT_NEUTRAL
+};
+
+void pl_chroma_location_offset(enum pl_chroma_location loc, float *x, float *y)
+{
+ *x = *y = 0;
+
+ // This is the majority of subsampled chroma content out there
+ loc = PL_DEF(loc, PL_CHROMA_LEFT);
+
+ switch (loc) {
+ case PL_CHROMA_LEFT:
+ case PL_CHROMA_TOP_LEFT:
+ case PL_CHROMA_BOTTOM_LEFT:
+ *x = -0.5;
+ break;
+ default: break;
+ }
+
+ switch (loc) {
+ case PL_CHROMA_TOP_LEFT:
+ case PL_CHROMA_TOP_CENTER:
+ *y = -0.5;
+ break;
+ default: break;
+ }
+
+ switch (loc) {
+ case PL_CHROMA_BOTTOM_LEFT:
+ case PL_CHROMA_BOTTOM_CENTER:
+ *y = 0.5;
+ break;
+ default: break;
+ }
+}
+
+struct pl_cie_xy pl_white_from_temp(float temp)
+{
+ temp = PL_CLAMP(temp, 2500, 25000);
+
+ double ti = 1000.0 / temp, ti2 = ti * ti, ti3 = ti2 * ti, x;
+ if (temp <= 7000) {
+ x = -4.6070 * ti3 + 2.9678 * ti2 + 0.09911 * ti + 0.244063;
+ } else {
+ x = -2.0064 * ti3 + 1.9018 * ti2 + 0.24748 * ti + 0.237040;
+ }
+
+ return (struct pl_cie_xy) {
+ .x = x,
+ .y = -3 * (x*x) + 2.87 * x - 0.275,
+ };
+}
+
+bool pl_raw_primaries_equal(const struct pl_raw_primaries *a,
+ const struct pl_raw_primaries *b)
+{
+ return pl_cie_xy_equal(&a->red, &b->red) &&
+ pl_cie_xy_equal(&a->green, &b->green) &&
+ pl_cie_xy_equal(&a->blue, &b->blue) &&
+ pl_cie_xy_equal(&a->white, &b->white);
+}
+
+bool pl_raw_primaries_similar(const struct pl_raw_primaries *a,
+ const struct pl_raw_primaries *b)
+{
+ float delta = fabsf(a->red.x - b->red.x) +
+ fabsf(a->red.y - b->red.y) +
+ fabsf(a->green.x - b->green.x) +
+ fabsf(a->green.y - b->green.y) +
+ fabsf(a->blue.x - b->blue.x) +
+ fabsf(a->blue.y - b->blue.y) +
+ fabsf(a->white.x - b->white.x) +
+ fabsf(a->white.y - b->white.y);
+
+ return delta < 0.001;
+}
+
+void pl_raw_primaries_merge(struct pl_raw_primaries *orig,
+ const struct pl_raw_primaries *update)
+{
+ union {
+ struct pl_raw_primaries prim;
+ float raw[8];
+ } *pa = (void *) orig,
+ *pb = (void *) update;
+
+ pl_static_assert(sizeof(*pa) == sizeof(*orig));
+ for (int i = 0; i < PL_ARRAY_SIZE(pa->raw); i++)
+ pa->raw[i] = PL_DEF(pa->raw[i], pb->raw[i]);
+}
+
+const struct pl_raw_primaries *pl_raw_primaries_get(enum pl_color_primaries prim)
+{
+ /*
+ Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0
+
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.470-6-199811-S!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf
+ https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-0-201208-I!!PDF-E.pdf
+
+ Other colorspaces from https://en.wikipedia.org/wiki/RGB_color_space#Specifications
+ */
+
+ // CIE standard illuminant series
+#define CIE_D50 {0.3457, 0.3585}
+#define CIE_D65 {0.3127, 0.3290}
+#define CIE_C {0.3100, 0.3160}
+#define CIE_E {1.0/3.0, 1.0/3.0}
+#define DCI {0.3140, 0.3510}
+
+ static const struct pl_raw_primaries primaries[] = {
+ [PL_COLOR_PRIM_BT_470M] = {
+ .red = {0.670, 0.330},
+ .green = {0.210, 0.710},
+ .blue = {0.140, 0.080},
+ .white = CIE_C,
+ },
+
+ [PL_COLOR_PRIM_BT_601_525] = {
+ .red = {0.630, 0.340},
+ .green = {0.310, 0.595},
+ .blue = {0.155, 0.070},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_BT_601_625] = {
+ .red = {0.640, 0.330},
+ .green = {0.290, 0.600},
+ .blue = {0.150, 0.060},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_BT_709] = {
+ .red = {0.640, 0.330},
+ .green = {0.300, 0.600},
+ .blue = {0.150, 0.060},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_BT_2020] = {
+ .red = {0.708, 0.292},
+ .green = {0.170, 0.797},
+ .blue = {0.131, 0.046},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_APPLE] = {
+ .red = {0.625, 0.340},
+ .green = {0.280, 0.595},
+ .blue = {0.115, 0.070},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_ADOBE] = {
+ .red = {0.640, 0.330},
+ .green = {0.210, 0.710},
+ .blue = {0.150, 0.060},
+ .white = CIE_D65,
+ },
+ [PL_COLOR_PRIM_PRO_PHOTO] = {
+ .red = {0.7347, 0.2653},
+ .green = {0.1596, 0.8404},
+ .blue = {0.0366, 0.0001},
+ .white = CIE_D50,
+ },
+ [PL_COLOR_PRIM_CIE_1931] = {
+ .red = {0.7347, 0.2653},
+ .green = {0.2738, 0.7174},
+ .blue = {0.1666, 0.0089},
+ .white = CIE_E,
+ },
+ // From SMPTE RP 431-2
+ [PL_COLOR_PRIM_DCI_P3] = {
+ .red = {0.680, 0.320},
+ .green = {0.265, 0.690},
+ .blue = {0.150, 0.060},
+ .white = DCI,
+ },
+ [PL_COLOR_PRIM_DISPLAY_P3] = {
+ .red = {0.680, 0.320},
+ .green = {0.265, 0.690},
+ .blue = {0.150, 0.060},
+ .white = CIE_D65,
+ },
+ // From Panasonic VARICAM reference manual
+ [PL_COLOR_PRIM_V_GAMUT] = {
+ .red = {0.730, 0.280},
+ .green = {0.165, 0.840},
+ .blue = {0.100, -0.03},
+ .white = CIE_D65,
+ },
+ // From Sony S-Log reference manual
+ [PL_COLOR_PRIM_S_GAMUT] = {
+ .red = {0.730, 0.280},
+ .green = {0.140, 0.855},
+ .blue = {0.100, -0.05},
+ .white = CIE_D65,
+ },
+ // From FFmpeg source code
+ [PL_COLOR_PRIM_FILM_C] = {
+ .red = {0.681, 0.319},
+ .green = {0.243, 0.692},
+ .blue = {0.145, 0.049},
+ .white = CIE_C,
+ },
+ [PL_COLOR_PRIM_EBU_3213] = {
+ .red = {0.630, 0.340},
+ .green = {0.295, 0.605},
+ .blue = {0.155, 0.077},
+ .white = CIE_D65,
+ },
+ // From Wikipedia
+ [PL_COLOR_PRIM_ACES_AP0] = {
+ .red = {0.7347, 0.2653},
+ .green = {0.0000, 1.0000},
+ .blue = {0.0001, -0.0770},
+ .white = {0.32168, 0.33767},
+ },
+ [PL_COLOR_PRIM_ACES_AP1] = {
+ .red = {0.713, 0.293},
+ .green = {0.165, 0.830},
+ .blue = {0.128, 0.044},
+ .white = {0.32168, 0.33767},
+ },
+ };
+
+ // This is the default assumption if no colorspace information could
+ // be determined, eg. for files which have no video channel.
+ if (!prim)
+ prim = PL_COLOR_PRIM_BT_709;
+
+ pl_assert(prim < PL_ARRAY_SIZE(primaries));
+ return &primaries[prim];
+}
+
+// Compute the RGB/XYZ matrix as described here:
+// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+pl_matrix3x3 pl_get_rgb2xyz_matrix(const struct pl_raw_primaries *prim)
+{
+ pl_matrix3x3 out = {{{0}}};
+ float S[3], X[4], Z[4];
+
+ X[0] = pl_cie_X(prim->red);
+ X[1] = pl_cie_X(prim->green);
+ X[2] = pl_cie_X(prim->blue);
+ X[3] = pl_cie_X(prim->white);
+
+ Z[0] = pl_cie_Z(prim->red);
+ Z[1] = pl_cie_Z(prim->green);
+ Z[2] = pl_cie_Z(prim->blue);
+ Z[3] = pl_cie_Z(prim->white);
+
+ // S = XYZ^-1 * W
+ for (int i = 0; i < 3; i++) {
+ out.m[0][i] = X[i];
+ out.m[1][i] = 1;
+ out.m[2][i] = Z[i];
+ }
+
+ pl_matrix3x3_invert(&out);
+
+ for (int i = 0; i < 3; i++)
+ S[i] = out.m[i][0] * X[3] + out.m[i][1] * 1 + out.m[i][2] * Z[3];
+
+ // M = [Sc * XYZc]
+ for (int i = 0; i < 3; i++) {
+ out.m[0][i] = S[i] * X[i];
+ out.m[1][i] = S[i] * 1;
+ out.m[2][i] = S[i] * Z[i];
+ }
+
+ return out;
+}
+
+pl_matrix3x3 pl_get_xyz2rgb_matrix(const struct pl_raw_primaries *prim)
+{
+ // For simplicity, just invert the rgb2xyz matrix
+ pl_matrix3x3 out = pl_get_rgb2xyz_matrix(prim);
+ pl_matrix3x3_invert(&out);
+ return out;
+}
+
+// LMS<-XYZ revised matrix from CIECAM97, based on a linear transform and
+// normalized for equal energy on monochrome inputs
+static const pl_matrix3x3 m_cat97 = {{
+ { 0.8562, 0.3372, -0.1934 },
+ { -0.8360, 1.8327, 0.0033 },
+ { 0.0357, -0.0469, 1.0112 },
+}};
+
+// M := M * XYZd<-XYZs
+static void apply_chromatic_adaptation(struct pl_cie_xy src,
+ struct pl_cie_xy dest,
+ pl_matrix3x3 *mat)
+{
+ // If the white points are nearly identical, this is a wasteful identity
+ // operation.
+ if (fabs(src.x - dest.x) < 1e-6 && fabs(src.y - dest.y) < 1e-6)
+ return;
+
+ // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma
+ // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
+ // For Ma, we use the CIECAM97 revised (linear) matrix
+ float C[3][2];
+
+ for (int i = 0; i < 3; i++) {
+ // source cone
+ C[i][0] = m_cat97.m[i][0] * pl_cie_X(src)
+ + m_cat97.m[i][1] * 1
+ + m_cat97.m[i][2] * pl_cie_Z(src);
+
+ // dest cone
+ C[i][1] = m_cat97.m[i][0] * pl_cie_X(dest)
+ + m_cat97.m[i][1] * 1
+ + m_cat97.m[i][2] * pl_cie_Z(dest);
+ }
+
+ // tmp := I * [Cd/Cs] * Ma
+ pl_matrix3x3 tmp = {0};
+ for (int i = 0; i < 3; i++)
+ tmp.m[i][i] = C[i][1] / C[i][0];
+
+ pl_matrix3x3_mul(&tmp, &m_cat97);
+
+ // M := M * Ma^-1 * tmp
+ pl_matrix3x3 ma_inv = m_cat97;
+ pl_matrix3x3_invert(&ma_inv);
+ pl_matrix3x3_mul(mat, &ma_inv);
+ pl_matrix3x3_mul(mat, &tmp);
+}
+
+pl_matrix3x3 pl_get_adaptation_matrix(struct pl_cie_xy src, struct pl_cie_xy dst)
+{
+ // Use BT.709 primaries (with chosen white point) as an XYZ reference
+ struct pl_raw_primaries csp = *pl_raw_primaries_get(PL_COLOR_PRIM_BT_709);
+ csp.white = src;
+
+ pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&csp);
+ pl_matrix3x3 xyz2rgb = rgb2xyz;
+ pl_matrix3x3_invert(&xyz2rgb);
+
+ apply_chromatic_adaptation(src, dst, &xyz2rgb);
+ pl_matrix3x3_mul(&xyz2rgb, &rgb2xyz);
+ return xyz2rgb;
+}
+
+pl_matrix3x3 pl_ipt_rgb2lms(const struct pl_raw_primaries *prim)
+{
+ static const pl_matrix3x3 hpe = {{ // HPE XYZ->LMS (D65) method
+ { 0.40024f, 0.70760f, -0.08081f },
+ { -0.22630f, 1.16532f, 0.04570f },
+ { 0.00000f, 0.00000f, 0.91822f },
+ }};
+
+ const float c = 0.04; // 4% crosstalk
+ pl_matrix3x3 m = {{
+ { 1 - 2*c, c, c },
+ { c, 1 - 2*c, c },
+ { c, c, 1 - 2*c },
+ }};
+
+ pl_matrix3x3_mul(&m, &hpe);
+
+ // Apply chromatic adaptation to D65 if the input white point differs
+ static const struct pl_cie_xy d65 = CIE_D65;
+ apply_chromatic_adaptation(prim->white, d65, &m);
+
+ const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim);
+ pl_matrix3x3_mul(&m, &rgb2xyz);
+ return m;
+}
+
+pl_matrix3x3 pl_ipt_lms2rgb(const struct pl_raw_primaries *prim)
+{
+ pl_matrix3x3 m = pl_ipt_rgb2lms(prim);
+ pl_matrix3x3_invert(&m);
+ return m;
+}
+
+// As standardized in Ebner & Fairchild IPT (1998)
+const pl_matrix3x3 pl_ipt_lms2ipt = {{
+ { 0.4000, 0.4000, 0.2000 },
+ { 4.4550, -4.8510, 0.3960 },
+ { 0.8056, 0.3572, -1.1628 },
+}};
+
+// Numerically inverted from the matrix above
+const pl_matrix3x3 pl_ipt_ipt2lms = {{
+ { 1.0, 0.0975689, 0.205226 },
+ { 1.0, -0.1138760, 0.133217 },
+ { 1.0, 0.0326151, -0.676887 },
+}};
+
+const struct pl_cone_params pl_vision_normal = {PL_CONE_NONE, 1.0};
+const struct pl_cone_params pl_vision_protanomaly = {PL_CONE_L, 0.5};
+const struct pl_cone_params pl_vision_protanopia = {PL_CONE_L, 0.0};
+const struct pl_cone_params pl_vision_deuteranomaly = {PL_CONE_M, 0.5};
+const struct pl_cone_params pl_vision_deuteranopia = {PL_CONE_M, 0.0};
+const struct pl_cone_params pl_vision_tritanomaly = {PL_CONE_S, 0.5};
+const struct pl_cone_params pl_vision_tritanopia = {PL_CONE_S, 0.0};
+const struct pl_cone_params pl_vision_monochromacy = {PL_CONE_LM, 0.0};
+const struct pl_cone_params pl_vision_achromatopsia = {PL_CONE_LMS, 0.0};
+
+pl_matrix3x3 pl_get_cone_matrix(const struct pl_cone_params *params,
+ const struct pl_raw_primaries *prim)
+{
+ // LMS<-RGB := LMS<-XYZ * XYZ<-RGB
+ pl_matrix3x3 rgb2lms = m_cat97;
+ pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim);
+ pl_matrix3x3_mul(&rgb2lms, &rgb2xyz);
+
+ // LMS versions of the two opposing primaries, plus neutral
+ float lms_r[3] = {1.0, 0.0, 0.0},
+ lms_b[3] = {0.0, 0.0, 1.0},
+ lms_w[3] = {1.0, 1.0, 1.0};
+
+ pl_matrix3x3_apply(&rgb2lms, lms_r);
+ pl_matrix3x3_apply(&rgb2lms, lms_b);
+ pl_matrix3x3_apply(&rgb2lms, lms_w);
+
+ float a, b, c = params->strength;
+ pl_matrix3x3 distort;
+
+ switch (params->cones) {
+ case PL_CONE_NONE:
+ return pl_matrix3x3_identity;
+
+ case PL_CONE_L:
+ // Solve to preserve neutral and blue
+ a = (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]) /
+ (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]);
+ b = (lms_b[0] - lms_b[1] * lms_w[0] / lms_w[1]) /
+ (lms_b[2] - lms_b[1] * lms_w[2] / lms_w[1]);
+ assert(fabs(a * lms_w[1] + b * lms_w[2] - lms_w[0]) < 1e-6);
+
+ distort = (pl_matrix3x3) {{
+ { c, (1.0 - c) * a, (1.0 - c) * b},
+ { 0.0, 1.0, 0.0},
+ { 0.0, 0.0, 1.0},
+ }};
+ break;
+
+ case PL_CONE_M:
+ // Solve to preserve neutral and blue
+ a = (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]) /
+ (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]);
+ b = (lms_b[1] - lms_b[0] * lms_w[1] / lms_w[0]) /
+ (lms_b[2] - lms_b[0] * lms_w[2] / lms_w[0]);
+ assert(fabs(a * lms_w[0] + b * lms_w[2] - lms_w[1]) < 1e-6);
+
+ distort = (pl_matrix3x3) {{
+ { 1.0, 0.0, 0.0},
+ {(1.0 - c) * a, c, (1.0 - c) * b},
+ { 0.0, 0.0, 1.0},
+ }};
+ break;
+
+ case PL_CONE_S:
+ // Solve to preserve neutral and red
+ a = (lms_r[2] - lms_r[1] * lms_w[2] / lms_w[1]) /
+ (lms_r[0] - lms_r[1] * lms_w[0] / lms_w[1]);
+ b = (lms_r[2] - lms_r[0] * lms_w[2] / lms_w[0]) /
+ (lms_r[1] - lms_r[0] * lms_w[1] / lms_w[0]);
+ assert(fabs(a * lms_w[0] + b * lms_w[1] - lms_w[2]) < 1e-6);
+
+ distort = (pl_matrix3x3) {{
+ { 1.0, 0.0, 0.0},
+ { 0.0, 1.0, 0.0},
+ {(1.0 - c) * a, (1.0 - c) * b, c},
+ }};
+ break;
+
+ case PL_CONE_LM:
+ // Solve to preserve neutral
+ a = lms_w[0] / lms_w[2];
+ b = lms_w[1] / lms_w[2];
+
+ distort = (pl_matrix3x3) {{
+ { c, 0.0, (1.0 - c) * a},
+ { 0.0, c, (1.0 - c) * b},
+ { 0.0, 0.0, 1.0},
+ }};
+ break;
+
+ case PL_CONE_MS:
+ // Solve to preserve neutral
+ a = lms_w[1] / lms_w[0];
+ b = lms_w[2] / lms_w[0];
+
+ distort = (pl_matrix3x3) {{
+ { 1.0, 0.0, 0.0},
+ {(1.0 - c) * a, c, 0.0},
+ {(1.0 - c) * b, 0.0, c},
+ }};
+ break;
+
+ case PL_CONE_LS:
+ // Solve to preserve neutral
+ a = lms_w[0] / lms_w[1];
+ b = lms_w[2] / lms_w[1];
+
+ distort = (pl_matrix3x3) {{
+ { c, (1.0 - c) * a, 0.0},
+ { 0.0, 1.0, 0.0},
+ { 0.0, (1.0 - c) * b, c},
+ }};
+ break;
+
+ case PL_CONE_LMS: {
+ // Rod cells only, which can be modelled somewhat as a combination of
+ // L and M cones. Either way, this is pushing the limits of the our
+ // color model, so this is only a rough approximation.
+ const float w[3] = {0.3605, 0.6415, -0.002};
+ assert(fabs(w[0] + w[1] + w[2] - 1.0) < 1e-6);
+
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ distort.m[i][j] = (1.0 - c) * w[j] * lms_w[i] / lms_w[j];
+ if (i == j)
+ distort.m[i][j] += c;
+ }
+ }
+ break;
+ }
+
+ default:
+ pl_unreachable();
+ }
+
+ // out := RGB<-LMS * distort * LMS<-RGB
+ pl_matrix3x3 out = rgb2lms;
+ pl_matrix3x3_invert(&out);
+ pl_matrix3x3_mul(&out, &distort);
+ pl_matrix3x3_mul(&out, &rgb2lms);
+
+ return out;
+}
+
+pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *src,
+ const struct pl_raw_primaries *dst,
+ enum pl_rendering_intent intent)
+{
+ // In saturation mapping, we don't care about accuracy and just want
+ // primaries to map to primaries, making this an identity transformation.
+ if (intent == PL_INTENT_SATURATION)
+ return pl_matrix3x3_identity;
+
+ // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs
+ // Equations from: http://www.brucelindbloom.com/index.html?Math.html
+ // Note: Perceptual is treated like relative colorimetric. There's no
+ // definition for perceptual other than "make it look good".
+
+ // RGBd<-XYZd matrix
+ pl_matrix3x3 xyz2rgb_d = pl_get_xyz2rgb_matrix(dst);
+
+ // Chromatic adaptation, except in absolute colorimetric intent
+ if (intent != PL_INTENT_ABSOLUTE_COLORIMETRIC)
+ apply_chromatic_adaptation(src->white, dst->white, &xyz2rgb_d);
+
+ // XYZs<-RGBs
+ pl_matrix3x3 rgb2xyz_s = pl_get_rgb2xyz_matrix(src);
+ pl_matrix3x3_mul(&xyz2rgb_d, &rgb2xyz_s);
+ return xyz2rgb_d;
+}
+
+// Test the sign of 'p' relative to the line 'ab' (barycentric coordinates)
+static float test_point_line(const struct pl_cie_xy p,
+ const struct pl_cie_xy a,
+ const struct pl_cie_xy b)
+{
+ return (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
+}
+
+// Test if a point is entirely inside a gamut
+static float test_point_gamut(struct pl_cie_xy point,
+ const struct pl_raw_primaries *prim)
+{
+ float d1 = test_point_line(point, prim->red, prim->green),
+ d2 = test_point_line(point, prim->green, prim->blue),
+ d3 = test_point_line(point, prim->blue, prim->red);
+
+ bool has_neg = d1 < -1e-6f || d2 < -1e-6f || d3 < -1e-6f,
+ has_pos = d1 > 1e-6f || d2 > 1e-6f || d3 > 1e-6f;
+
+ return !(has_neg && has_pos);
+}
+
+bool pl_primaries_superset(const struct pl_raw_primaries *a,
+ const struct pl_raw_primaries *b)
+{
+ return test_point_gamut(b->red, a) &&
+ test_point_gamut(b->green, a) &&
+ test_point_gamut(b->blue, a);
+}
+
+bool pl_primaries_valid(const struct pl_raw_primaries *prim)
+{
+ // Test to see if the primaries form a valid triangle (nonzero area)
+ float area = (prim->blue.x - prim->green.x) * (prim->red.y - prim->green.y)
+ - (prim->red.x - prim->green.x) * (prim->blue.y - prim->green.y);
+
+ return fabs(area) > 1e-6 && test_point_gamut(prim->white, prim);
+}
+
+static inline float xy_dist2(struct pl_cie_xy a, struct pl_cie_xy b)
+{
+ const float dx = a.x - b.x, dy = a.y - b.y;
+ return dx * dx + dy * dy;
+}
+
+bool pl_primaries_compatible(const struct pl_raw_primaries *a,
+ const struct pl_raw_primaries *b)
+{
+ float RR = xy_dist2(a->red, b->red), RG = xy_dist2(a->red, b->green),
+ RB = xy_dist2(a->red, b->blue), GG = xy_dist2(a->green, b->green),
+ GB = xy_dist2(a->green, b->blue), BB = xy_dist2(a->blue, b->blue);
+ return RR < RG && RR < RB && GG < RG && GG < GB && BB < RB && BB < GB;
+}
+
+// returns the intersection of the two lines defined by ab and cd
+static struct pl_cie_xy intersection(struct pl_cie_xy a, struct pl_cie_xy b,
+ struct pl_cie_xy c, struct pl_cie_xy d)
+{
+ float det = (a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x);
+ float t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / det;
+ return (struct pl_cie_xy) {
+ .x = t ? a.x + t * (b.x - a.x) : 0.0f,
+ .y = t ? a.y + t * (b.y - a.y) : 0.0f,
+ };
+}
+
+// x, y, z specified in clockwise order, with a, b, c being the enclosing gamut
+static struct pl_cie_xy
+clip_point(struct pl_cie_xy x, struct pl_cie_xy y, struct pl_cie_xy z,
+ struct pl_cie_xy a, struct pl_cie_xy b, struct pl_cie_xy c)
+{
+ const float d1 = test_point_line(y, a, b);
+ const float d2 = test_point_line(y, b, c);
+ if (d1 <= 0.0f && d2 <= 0.0f) {
+ return y; // already inside triangle
+ } else if (d1 > 0.0f && d2 > 0.0f) {
+ return b; // target vertex fully enclosed
+ } else if (d1 > 0.0f) {
+ return intersection(a, b, y, z);
+ } else {
+ return intersection(x, y, b, c);
+ }
+}
+
+struct pl_raw_primaries pl_primaries_clip(const struct pl_raw_primaries *src,
+ const struct pl_raw_primaries *dst)
+{
+ return (struct pl_raw_primaries) {
+ .red = clip_point(src->green, src->red, src->blue,
+ dst->green, dst->red, dst->blue),
+ .green = clip_point(src->blue, src->green, src->red,
+ dst->blue, dst->green, dst->red),
+ .blue = clip_point(src->red, src->blue, src->green,
+ dst->red, dst->blue, dst->green),
+ .white = src->white,
+ };
+}
+
+/* Fill in the Y, U, V vectors of a yuv-to-rgb conversion matrix
+ * based on the given luma weights of the R, G and B components (lr, lg, lb).
+ * lr+lg+lb is assumed to equal 1.
+ * This function is meant for colorspaces satisfying the following
+ * conditions (which are true for common YUV colorspaces):
+ * - The mapping from input [Y, U, V] to output [R, G, B] is linear.
+ * - Y is the vector [1, 1, 1]. (meaning input Y component maps to 1R+1G+1B)
+ * - U maps to a value with zero R and positive B ([0, x, y], y > 0;
+ * i.e. blue and green only).
+ * - V maps to a value with zero B and positive R ([x, y, 0], x > 0;
+ * i.e. red and green only).
+ * - U and V are orthogonal to the luma vector [lr, lg, lb].
+ * - The magnitudes of the vectors U and V are the minimal ones for which
+ * the image of the set Y=[0...1],U=[-0.5...0.5],V=[-0.5...0.5] under the
+ * conversion function will cover the set R=[0...1],G=[0...1],B=[0...1]
+ * (the resulting matrix can be converted for other input/output ranges
+ * outside this function).
+ * Under these conditions the given parameters lr, lg, lb uniquely
+ * determine the mapping of Y, U, V to R, G, B.
+ */
+static pl_matrix3x3 luma_coeffs(float lr, float lg, float lb)
+{
+ pl_assert(fabs(lr+lg+lb - 1) < 1e-6);
+ return (pl_matrix3x3) {{
+ {1, 0, 2 * (1-lr) },
+ {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg },
+ {1, 2 * (1-lb), 0 },
+ }};
+}
+
+// Applies hue and saturation controls to a YCbCr->RGB matrix
+static inline void apply_hue_sat(pl_matrix3x3 *m,
+ const struct pl_color_adjustment *params)
+{
+ // Hue is equivalent to rotating input [U, V] subvector around the origin.
+ // Saturation scales [U, V].
+ float huecos = params->saturation * cos(params->hue);
+ float huesin = params->saturation * sin(params->hue);
+ for (int i = 0; i < 3; i++) {
+ float u = m->m[i][1], v = m->m[i][2];
+ m->m[i][1] = huecos * u - huesin * v;
+ m->m[i][2] = huesin * u + huecos * v;
+ }
+}
+
+pl_transform3x3 pl_color_repr_decode(struct pl_color_repr *repr,
+ const struct pl_color_adjustment *params)
+{
+ params = PL_DEF(params, &pl_color_adjustment_neutral);
+
+ pl_matrix3x3 m;
+ switch (repr->sys) {
+ case PL_COLOR_SYSTEM_BT_709: m = luma_coeffs(0.2126, 0.7152, 0.0722); break;
+ case PL_COLOR_SYSTEM_BT_601: m = luma_coeffs(0.2990, 0.5870, 0.1140); break;
+ case PL_COLOR_SYSTEM_SMPTE_240M: m = luma_coeffs(0.2122, 0.7013, 0.0865); break;
+ case PL_COLOR_SYSTEM_BT_2020_NC: m = luma_coeffs(0.2627, 0.6780, 0.0593); break;
+ case PL_COLOR_SYSTEM_BT_2020_C:
+ // Note: This outputs into the [-0.5,0.5] range for chroma information.
+ m = (pl_matrix3x3) {{
+ {0, 0, 1},
+ {1, 0, 0},
+ {0, 1, 0},
+ }};
+ break;
+ case PL_COLOR_SYSTEM_BT_2100_PQ: {
+ // Reversed from the matrix in the spec, hard-coded for efficiency
+ // and precision reasons. Exact values truncated from ITU-T H-series
+ // Supplement 18.
+ static const float lm_t = 0.008609, lm_p = 0.111029625;
+ m = (pl_matrix3x3) {{
+ {1.0, lm_t, lm_p},
+ {1.0, -lm_t, -lm_p},
+ {1.0, 0.560031, -0.320627},
+ }};
+ break;
+ }
+ case PL_COLOR_SYSTEM_BT_2100_HLG: {
+ // Similar to BT.2100 PQ, exact values truncated from WolframAlpha
+ static const float lm_t = 0.01571858011, lm_p = 0.2095810681;
+ m = (pl_matrix3x3) {{
+ {1.0, lm_t, lm_p},
+ {1.0, -lm_t, -lm_p},
+ {1.0, 1.02127108, -0.605274491},
+ }};
+ break;
+ }
+ case PL_COLOR_SYSTEM_DOLBYVISION:
+ m = repr->dovi->nonlinear;
+ break;
+ case PL_COLOR_SYSTEM_YCGCO:
+ m = (pl_matrix3x3) {{
+ {1, -1, 1},
+ {1, 1, 0},
+ {1, -1, -1},
+ }};
+ break;
+ case PL_COLOR_SYSTEM_UNKNOWN: // fall through
+ case PL_COLOR_SYSTEM_RGB:
+ m = pl_matrix3x3_identity;
+ break;
+ case PL_COLOR_SYSTEM_XYZ: {
+ // For lack of anything saner to do, just assume the caller wants
+ // DCI-P3 primaries, which is a reasonable assumption.
+ const struct pl_raw_primaries *dst = pl_raw_primaries_get(PL_COLOR_PRIM_DCI_P3);
+ m = pl_get_xyz2rgb_matrix(dst);
+ // DCDM X'Y'Z' is expected to have equal energy white point (EG 432-1 Annex H)
+ apply_chromatic_adaptation((struct pl_cie_xy)CIE_E, dst->white, &m);
+ break;
+ }
+ case PL_COLOR_SYSTEM_COUNT:
+ pl_unreachable();
+ }
+
+ // Apply hue and saturation in the correct way depending on the colorspace.
+ if (pl_color_system_is_ycbcr_like(repr->sys)) {
+ apply_hue_sat(&m, params);
+ } else if (params->saturation != 1.0 || params->hue != 0.0) {
+ // Arbitrarily simulate hue shifts using the BT.709 YCbCr model
+ pl_matrix3x3 yuv2rgb = luma_coeffs(0.2126, 0.7152, 0.0722);
+ pl_matrix3x3 rgb2yuv = yuv2rgb;
+ pl_matrix3x3_invert(&rgb2yuv);
+ apply_hue_sat(&yuv2rgb, params);
+ // M := RGB<-YUV * YUV<-RGB * M
+ pl_matrix3x3_rmul(&rgb2yuv, &m);
+ pl_matrix3x3_rmul(&yuv2rgb, &m);
+ }
+
+ // Apply color temperature adaptation, relative to BT.709 primaries
+ if (params->temperature) {
+ struct pl_cie_xy src = pl_white_from_temp(6500);
+ struct pl_cie_xy dst = pl_white_from_temp(6500 + 3500 * params->temperature);
+ pl_matrix3x3 adapt = pl_get_adaptation_matrix(src, dst);
+ pl_matrix3x3_rmul(&adapt, &m);
+ }
+
+ pl_transform3x3 out = { .mat = m };
+ int bit_depth = PL_DEF(repr->bits.sample_depth,
+ PL_DEF(repr->bits.color_depth, 8));
+
+ double ymax, ymin, cmax, cmid;
+ double scale = (1LL << bit_depth) / ((1LL << bit_depth) - 1.0);
+
+ switch (pl_color_levels_guess(repr)) {
+ case PL_COLOR_LEVELS_LIMITED: {
+ ymax = 235 / 256. * scale;
+ ymin = 16 / 256. * scale;
+ cmax = 240 / 256. * scale;
+ cmid = 128 / 256. * scale;
+ break;
+ }
+ case PL_COLOR_LEVELS_FULL:
+ // Note: For full-range YUV, there are multiple, subtly inconsistent
+ // standards. So just pick the sanest implementation, which is to
+ // assume MAX_INT == 1.0.
+ ymax = 1.0;
+ ymin = 0.0;
+ cmax = 1.0;
+ cmid = 128 / 256. * scale; // *not* exactly 0.5
+ break;
+ default:
+ pl_unreachable();
+ }
+
+ double ymul = 1.0 / (ymax - ymin);
+ double cmul = 0.5 / (cmax - cmid);
+
+ double mul[3] = { ymul, ymul, ymul };
+ double black[3] = { ymin, ymin, ymin };
+
+#ifdef PL_HAVE_DOVI
+ if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION) {
+ // The RPU matrix already includes levels normalization, but in this
+ // case we also have to respect the signalled color offsets
+ for (int i = 0; i < 3; i++) {
+ mul[i] = 1.0;
+ black[i] = repr->dovi->nonlinear_offset[i] * scale;
+ }
+ } else
+#endif
+ if (pl_color_system_is_ycbcr_like(repr->sys)) {
+ mul[1] = mul[2] = cmul;
+ black[1] = black[2] = cmid;
+ }
+
+ // Contrast scales the output value range (gain)
+ // Brightness scales the constant output bias (black lift/boost)
+ for (int i = 0; i < 3; i++) {
+ mul[i] *= params->contrast;
+ out.c[i] += params->brightness;
+ }
+
+ // Multiply in the texture multiplier and adjust `c` so that black[j] keeps
+ // on mapping to RGB=0 (black to black)
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ out.mat.m[i][j] *= mul[j];
+ out.c[i] -= out.mat.m[i][j] * black[j];
+ }
+ }
+
+ // Finally, multiply in the scaling factor required to get the color up to
+ // the correct representation.
+ pl_matrix3x3_scale(&out.mat, pl_color_repr_normalize(repr));
+
+ // Update the metadata to reflect the change.
+ repr->sys = PL_COLOR_SYSTEM_RGB;
+ repr->levels = PL_COLOR_LEVELS_FULL;
+
+ return out;
+}
+
+bool pl_icc_profile_equal(const struct pl_icc_profile *p1,
+ const struct pl_icc_profile *p2)
+{
+ if (p1->len != p2->len)
+ return false;
+
+ // Ignore signatures on length-0 profiles, as a special case
+ return !p1->len || p1->signature == p2->signature;
+}
+
+void pl_icc_profile_compute_signature(struct pl_icc_profile *profile)
+{
+ if (!profile->len)
+ profile->signature = 0;
+
+ // In theory, we could get this value from the profile header itself if
+ // lcms is available, but I'm not sure if it's even worth the trouble. Just
+ // hard-code this to a pl_mem_hash(), which is decently fast anyway.
+ profile->signature = pl_mem_hash(profile->data, profile->len);
+}