/* * colors.c * Copyright (C) 2016 Kovid Goyal * * Distributed under terms of the GPL3 license. */ #include "state.h" #include #include "colors.h" #include "color-names.h" #ifdef __APPLE__ // Needed for strod_l #include #endif static const color_type NULL_COLOR_VALUE = 0xffffffff; static const color_type GENERATED_COLOR_MASK = 0xff000000; static uint32_t FG_BG_256[256] = { 0x000000, // 0 0xcd0000, // 1 0x00cd00, // 2 0xcdcd00, // 3 0x0000ee, // 4 0xcd00cd, // 5 0x00cdcd, // 6 0xe5e5e5, // 7 0x7f7f7f, // 8 0xff0000, // 9 0x00ff00, // 10 0xffff00, // 11 0x5c5cff, // 12 0xff00ff, // 13 0x00ffff, // 14 0xffffff, // 15 }; static void init_FG_BG_table(void) { if (UNLIKELY(FG_BG_256[255] == 0)) { // colors 16..232: the 6x6x6 color cube const uint8_t valuerange[6] = {0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff}; uint8_t i, j=16; for(i = 0; i < 216; i++, j++) { uint8_t r = valuerange[(i / 36) % 6], g = valuerange[(i / 6) % 6], b = valuerange[i % 6]; FG_BG_256[j] = (r << 16) | (g << 8) | b; } // colors 232..255: grayscale for(i = 0; i < 24; i++, j++) { uint8_t v = 8 + i * 10; FG_BG_256[j] = (v << 16) | (v << 8) | v; } } } static PyObject* create_256_color_table(void) { init_FG_BG_table(); PyObject *ans = PyTuple_New(arraysz(FG_BG_256)); if (ans == NULL) return PyErr_NoMemory(); for (size_t i=0; i < arraysz(FG_BG_256); i++) { PyObject *temp = PyLong_FromUnsignedLong(FG_BG_256[i]); if (temp == NULL) { Py_CLEAR(ans); return NULL; } PyTuple_SET_ITEM(ans, i, temp); } return ans; } static void set_transparent_background_colors(TransparentDynamicColor *dest, PyObject *src) { memset(dest, 0, sizeof(((ColorProfile*)0)->configured_transparent_colors)); for (Py_ssize_t i = 0; i < MIN(PyTuple_GET_SIZE(src), (Py_ssize_t)arraysz(((ColorProfile*)0)->configured_transparent_colors)); i++) { PyObject *e = PyTuple_GET_ITEM(src, i); dest[i].color = ((Color*)(PyTuple_GET_ITEM(e, 0)))->color.val & 0xffffff; dest[i].opacity = (float)PyFloat_AsDouble(PyTuple_GET_ITEM(e, 1)); dest[i].is_set = true; } } static bool set_configured_colors(ColorProfile *self, PyObject *opts) { #define n(which, attr) { \ RAII_PyObject(t, PyObject_GetAttrString(opts, #attr)); \ if (t == NULL) return false; \ if (t == Py_None) { self->configured.which.rgb = 0; self->configured.which.type = COLOR_IS_SPECIAL; } \ else if (PyLong_Check(t)) { \ unsigned int x = PyLong_AsUnsignedLong(t); \ self->configured.which.rgb = x & 0xffffff; \ self->configured.which.type = COLOR_IS_RGB; \ } else if (PyObject_TypeCheck(t, &Color_Type)) { \ Color *c = (Color*)t; \ self->configured.which.rgb = c->color.rgb; \ self->configured.which.type = COLOR_IS_RGB; \ } else { PyErr_SetString(PyExc_TypeError, "colors must be integers or Color objects"); return false; } \ } n(default_fg, foreground); n(default_bg, background); n(cursor_color, cursor); n(cursor_text_color, cursor_text_color); n(highlight_fg, selection_foreground); n(highlight_bg, selection_background); n(visual_bell_color, visual_bell_color); #undef n RAII_PyObject(src, PyObject_GetAttrString(opts, "transparent_background_colors")); if (!src) { PyErr_SetString(PyExc_TypeError, "No transparent_background_colors on opts object"); return false; } set_transparent_background_colors(self->configured_transparent_colors, src); return PyErr_Occurred() ? false : true; } static bool set_mark_colors(ColorProfile *self, PyObject *opts) { char fgattr[] = "mark?_foreground", bgattr[] = "mark?_background"; #define n(i, attr, which) { \ attr[4] = '1' + i; \ RAII_PyObject(t, PyObject_GetAttrString(opts, attr)); \ if (t == NULL) return false; \ if (!PyObject_TypeCheck(t, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "mark color is not Color object"); return false; } \ Color *c = (Color*)t; self->which[i] = c->color.rgb; \ } #define m(i) n(i, fgattr, mark_foregrounds); n(i, bgattr, mark_backgrounds); m(0); m(1); m(2); #undef m #undef n return true; } // Generating 256 color palette {{{ // For more information, see // https://gist.github.com/jake-stewart/0a8ea46159a7da2c808e5be2177e1783 static void color_type_to_lab(color_type color, float *lab) { float r = ((color >> 16) & 0xff) / 255.0f; float g = ((color >> 8) & 0xff) / 255.0f; float b = (color & 0xff) / 255.0f; r = r > 0.04045f ? powf((r + 0.055f) / 1.055f, 2.4f) : r / 12.92f; g = g > 0.04045f ? powf((g + 0.055f) / 1.055f, 2.4f) : g / 12.92f; b = b > 0.04045f ? powf((b + 0.055f) / 1.055f, 2.4f) : b / 12.92f; float x = (r * 0.4124564f + g * 0.3575761f + b * 0.1804375f) / 0.95047f; float y = (r * 0.2126729f + g * 0.7151522f + b * 0.0721750f); float z = (r * 0.0193339f + g * 0.1191920f + b * 0.9503041f) / 1.08883f; x = x > 0.008856f ? cbrtf(x) : 7.787f * x + 16.0f / 116.0f; y = y > 0.008856f ? cbrtf(y) : 7.787f * y + 16.0f / 116.0f; z = z > 0.008856f ? cbrtf(z) : 7.787f * z + 16.0f / 116.0f; lab[0] = 116.0f * y - 16.0f; lab[1] = 500.0f * (x - y); lab[2] = 200.0f * (y - z); } static color_type lab_to_color_type(float *lab) { float y = (lab[0] + 16.0f) / 116.0f; float x = lab[1] / 500.0f + y; float z = y - lab[2] / 200.0f; float x3 = x * x * x, y3 = y * y * y, z3 = z * z * z; x = (x3 > 0.008856f ? x3 : (x - 16.0f / 116.0f) / 7.787f) * 0.95047f; y = y3 > 0.008856f ? y3 : (y - 16.0f / 116.0f) / 7.787f; z = (z3 > 0.008856f ? z3 : (z - 16.0f / 116.0f) / 7.787f) * 1.08883f; float r = x * 3.2404542f - y * 1.5371385f - z * 0.4985314f; float g = -x * 0.9692660f + y * 1.8760108f + z * 0.0415560f; float b = x * 0.0556434f - y * 0.2040259f + z * 1.0572252f; r = r > 0.0031308f ? 1.055f * powf(r, 1.0f / 2.4f) - 0.055f : 12.92f * r; g = g > 0.0031308f ? 1.055f * powf(g, 1.0f / 2.4f) - 0.055f : 12.92f * g; b = b > 0.0031308f ? 1.055f * powf(b, 1.0f / 2.4f) - 0.055f : 12.92f * b; uint8_t rb = (uint8_t)(fminf(fmaxf(r, 0.0f), 1.0f) * 255.0f + 0.5f); uint8_t gb = (uint8_t)(fminf(fmaxf(g, 0.0f), 1.0f) * 255.0f + 0.5f); uint8_t bb = (uint8_t)(fminf(fmaxf(b, 0.0f), 1.0f) * 255.0f + 0.5f); return GENERATED_COLOR_MASK | (rb << 16) | (gb << 8) | bb; } static void lerp_lab(float t, float *a, float *b, float *out) { out[0] = a[0] + t * (b[0] - a[0]); out[1] = a[1] + t * (b[1] - a[1]); out[2] = a[2] + t * (b[2] - a[2]); } static void setup_256_palette_lab(ColorProfile *self, color_type *color_table, bool semantic, float base8_lab[8][3]) { const color_type bg = colorprofile_to_color(self, self->overridden.default_bg, self->configured.default_bg).rgb; const color_type fg = colorprofile_to_color(self, self->overridden.default_fg, self->configured.default_fg).rgb; color_type_to_lab(bg, base8_lab[0]); color_type_to_lab(fg, base8_lab[7]); for (int i = 1; i < 7; i++) color_type_to_lab(color_table[i], base8_lab[i]); bool is_light_theme = base8_lab[7][0] < base8_lab[0][0]; bool invert = is_light_theme && !semantic; if (invert) { float tmp[3]; memcpy(tmp, base8_lab[0], sizeof(tmp)); memcpy(base8_lab[0], base8_lab[7], sizeof(tmp)); memcpy(base8_lab[7], tmp, sizeof(tmp)); } } static bool is_generated_color(const color_type c) { return (c & GENERATED_COLOR_MASK) == GENERATED_COLOR_MASK; } static void generate_256_palette(ColorProfile *self, color_type *color_table, bool semantic) { float base8_lab[8][3]; setup_256_palette_lab(self, color_table, semantic, base8_lab); int idx = 16; for (int r = 0; r < 6; r++) { float c0[3], c1[3], c2[3], c3[3]; float tr = r / 5.0f; lerp_lab(tr, base8_lab[0], base8_lab[1], c0); lerp_lab(tr, base8_lab[2], base8_lab[3], c1); lerp_lab(tr, base8_lab[4], base8_lab[5], c2); lerp_lab(tr, base8_lab[6], base8_lab[7], c3); for (int g = 0; g < 6; g++) { float c4[3], c5[3]; float tg = g / 5.0f; lerp_lab(tg, c0, c1, c4); lerp_lab(tg, c2, c3, c5); for (int b = 0; b < 6; b++) { if (is_generated_color(color_table[idx])) { float c6[3]; lerp_lab(b / 5.0f, c4, c5, c6); color_table[idx] = lab_to_color_type(c6); } idx++; } } } for (int i = 0; i < 24; i++) { float t = (i + 1) / 25.0f; float lab[3]; lerp_lab(t, base8_lab[0], base8_lab[7], lab); if (is_generated_color(color_table[idx])) color_table[idx] = lab_to_color_type(lab); idx++; } } static color_type generate_256_palette_color(ColorProfile *self, color_type *color_table, unsigned int idx, bool semantic) { float base8_lab[8][3]; setup_256_palette_lab(self, color_table, semantic, base8_lab); if (idx < 232) { unsigned int i = idx - 16; int r = i / 36, g = (i % 36) / 6, b = i % 6; float c0[3], c1[3], c2[3], c3[3], c4[3], c5[3], c6[3]; lerp_lab(r / 5.0f, base8_lab[0], base8_lab[1], c0); lerp_lab(r / 5.0f, base8_lab[2], base8_lab[3], c1); lerp_lab(r / 5.0f, base8_lab[4], base8_lab[5], c2); lerp_lab(r / 5.0f, base8_lab[6], base8_lab[7], c3); lerp_lab(g / 5.0f, c0, c1, c4); lerp_lab(g / 5.0f, c2, c3, c5); lerp_lab(b / 5.0f, c4, c5, c6); return lab_to_color_type(c6); } else { int i = idx - 232; float lab[3]; lerp_lab((i + 1) / 25.0f, base8_lab[0], base8_lab[7], lab); return lab_to_color_type(lab); } } static void fixed_color_palette(color_type *color_table) { init_FG_BG_table(); for (unsigned i = 16; i < arraysz(FG_BG_256); i++) { if (is_generated_color(color_table[i])) color_table[i] = GENERATED_COLOR_MASK | FG_BG_256[i]; } } static bool palette_generation_is_dynamic(PyObject *opts, bool *semantic) { bool ans = false; *semantic = false; if (opts) { RAII_PyObject(policy, PyObject_GetAttrString(opts, "palette_generate")); if (!policy) return ans; switch(PyUnicode_AsUTF8(policy)[0]) { case 'f': break; case 's': *semantic = true; /* fallthrough */ default: ans = true; } } return ans; } // }}} static bool set_colortable(ColorProfile *self, PyObject *opts) { RAII_PyObject(ct, PyObject_GetAttrString(opts, "color_table")); if (!ct) return false; RAII_PyObject(ret, PyObject_CallMethod(ct, "buffer_info", NULL)); if (!ret) return false; bool semantic_generation, dynamic_palette = palette_generation_is_dynamic(opts, &semantic_generation); unsigned long *color_table = PyLong_AsVoidPtr(PyTuple_GET_ITEM(ret, 0)); size_t count = PyLong_AsSize_t(PyTuple_GET_ITEM(ret, 1)); if (!color_table || count != arraysz(FG_BG_256)) { PyErr_SetString(PyExc_TypeError, "color_table has incorrect length"); return false; } RAII_PyObject(r2, PyObject_GetAttrString(ct, "itemsize")); if (!r2) return false; size_t itemsize = PyLong_AsSize_t(r2); if (itemsize != sizeof(unsigned long)) { PyErr_Format(PyExc_TypeError, "color_table has incorrect itemsize: %zu", itemsize); return false; } for (size_t i = 0; i < arraysz(FG_BG_256); i++) self->color_table[i] = color_table[i]; if (dynamic_palette) generate_256_palette(self, self->color_table, semantic_generation); else fixed_color_palette(self->color_table); memcpy(self->orig_color_table, self->color_table, sizeof(self->color_table)); return true; } static PyObject* new_cp(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyObject *opts = global_state.options_object; ColorProfile *self; static const char* kw[] = {"opts", NULL}; if (args && !PyArg_ParseTupleAndKeywords(args, kwds, "|O", (char**)kw, &opts)) return NULL; self = (ColorProfile *)type->tp_alloc(type, 0); RAII_PyObject(ans, (PyObject*)self); if (self != NULL) { init_FG_BG_table(); if (opts) { if (!set_configured_colors(self, opts)) return NULL; if (!set_mark_colors(self, opts)) return NULL; if (!set_colortable(self, opts)) return NULL; } else { memcpy(self->color_table, FG_BG_256, sizeof(FG_BG_256)); memcpy(self->orig_color_table, self->color_table, sizeof(FG_BG_256)); } self->dirty = true; Py_INCREF(ans); } return ans; } static void dealloc_cp(ColorProfile* self) { if (self->color_stack) free(self->color_stack); Py_TYPE(self)->tp_free((PyObject*)self); } ColorProfile* alloc_color_profile(void) { return (ColorProfile*)new_cp(&ColorProfile_Type, NULL, NULL); } void copy_color_profile(ColorProfile *dest, ColorProfile *src) { memcpy(dest->color_table, src->color_table, sizeof(dest->color_table)); memcpy(dest->orig_color_table, src->orig_color_table, sizeof(dest->color_table)); memcpy(&dest->configured, &src->configured, sizeof(dest->configured)); memcpy(&dest->overridden, &src->overridden, sizeof(dest->overridden)); memcpy(dest->overriden_transparent_colors, src->overriden_transparent_colors, sizeof(dest->overriden_transparent_colors)); memcpy(dest->configured_transparent_colors, src->configured_transparent_colors, sizeof(dest->configured_transparent_colors)); dest->dirty = true; } static void patch_color_table(const char *key, PyObject *profiles, PyObject *spec, size_t which, int change_configured, bool *has_null_values) { PyObject *v = PyDict_GetItemString(spec, key); if (v) { color_type color = NULL_COLOR_VALUE; if (PyLong_Check(v)) color = PyLong_AsUnsignedLong(v); else *has_null_values = true; for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) { ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j); self->color_table[which] = color; if (change_configured) self->orig_color_table[which] = color; self->dirty = true; } } } #define patch_mark_color(key, profiles, spec, array, i) { \ PyObject *v = PyDict_GetItemString(spec, key); \ if (v && PyLong_Check(v)) { \ color_type color = PyLong_AsUnsignedLong(v); \ for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) { \ ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j); \ self->array[i] = color; \ self->dirty = true; \ } } } static PyObject* patch_color_profiles(PyObject *module UNUSED, PyObject *args) { PyObject *spec, *transparent_background_colors, *profiles, *v; ColorProfile *self; int change_configured; if (!PyArg_ParseTuple(args, "O!O!O!p", &PyDict_Type, &spec, &PyTuple_Type, &transparent_background_colors, &PyTuple_Type, &profiles, &change_configured)) return NULL; char key[32] = {0}; bool has_null_values = false; for (size_t i = 0; i < arraysz(FG_BG_256); i++) { snprintf(key, sizeof(key) - 1, "color%zu", i); patch_color_table(key, profiles, spec, i, change_configured, &has_null_values); } for (size_t i = 1; i <= MARK_MASK; i++) { #define S(which, i) snprintf(key, sizeof(key) - 1, "mark%zu_" #which, i); patch_mark_color(key, profiles, spec, mark_##which##s, i) S(background, i); S(foreground, i); #undef S } #define SI(profile_name) \ DynamicColor color; \ if (PyLong_Check(v)) { \ color.rgb = PyLong_AsUnsignedLong(v); color.type = COLOR_IS_RGB; \ } else { color.rgb = 0; color.type = COLOR_IS_SPECIAL; }\ self->overridden.profile_name = color; \ if (change_configured) self->configured.profile_name = color; \ self->dirty = true; #define S(config_name, profile_name) { \ v = PyDict_GetItemString(spec, #config_name); \ if (v) { \ for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { \ self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); \ SI(profile_name); \ } \ } \ } S(foreground, default_fg); S(background, default_bg); S(cursor, cursor_color); S(selection_foreground, highlight_fg); S(selection_background, highlight_bg); S(cursor_text_color, cursor_text_color); S(visual_bell_color, visual_bell_color); #undef SI #undef S for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(profiles); i++) { self = (ColorProfile*)PyTuple_GET_ITEM(profiles, i); set_transparent_background_colors(self->overriden_transparent_colors, transparent_background_colors); if (change_configured) set_transparent_background_colors(self->configured_transparent_colors, transparent_background_colors); } if (has_null_values) { bool semantic, dynamic = palette_generation_is_dynamic(global_state.options_object, &semantic); for (Py_ssize_t j = 0; j < PyTuple_GET_SIZE(profiles); j++) { ColorProfile *self = (ColorProfile*)PyTuple_GET_ITEM(profiles, j); if (dynamic) { generate_256_palette(self, self->color_table, semantic); if (change_configured) generate_256_palette(self, self->orig_color_table, semantic); } else { fixed_color_palette(self->color_table); if (change_configured) fixed_color_palette(self->orig_color_table); } } } if (PyErr_Occurred()) return NULL; Py_RETURN_NONE; } bool colorprofile_to_transparent_color(const ColorProfile *self, unsigned index, color_type *color, float *opacity) { *color = UINT32_MAX; *opacity = 1.0; if (index < arraysz(self->configured_transparent_colors)) { if (self->overriden_transparent_colors[index].is_set) { *color = self->overriden_transparent_colors[index].color; *opacity = self->overriden_transparent_colors[index].opacity; if (*opacity < 0) *opacity = OPT(background_opacity); return true; } if (self->configured_transparent_colors[index].is_set) { *color = self->configured_transparent_colors[index].color; *opacity = self->configured_transparent_colors[index].opacity; if (*opacity < 0) *opacity = OPT(background_opacity); return true; } } return false; } DynamicColor colorprofile_to_color(const ColorProfile *self, DynamicColor entry, DynamicColor defval) { switch(entry.type) { case COLOR_NOT_SET: return defval; case COLOR_IS_INDEX: { DynamicColor ans; ans.rgb = self->color_table[entry.rgb & 0xff] & (~GENERATED_COLOR_MASK); ans.type = COLOR_IS_RGB; return ans; } case COLOR_IS_RGB: case COLOR_IS_SPECIAL: return entry; } return entry; } color_type colorprofile_to_color_with_fallback(ColorProfile *self, DynamicColor entry, DynamicColor defval, DynamicColor fallback, DynamicColor fallback_defval) { switch(entry.type) { case COLOR_NOT_SET: case COLOR_IS_SPECIAL: if (defval.type == COLOR_IS_SPECIAL) return colorprofile_to_color(self, fallback, fallback_defval).rgb; return defval.rgb; case COLOR_IS_RGB: return entry.rgb; case COLOR_IS_INDEX: return self->color_table[entry.rgb & 0xff] & (~GENERATED_COLOR_MASK); } return entry.rgb; } static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a); static bool colortable_colors_into_dict(ColorProfile *self, unsigned start, unsigned limit, PyObject *ans) { static char buf[32] = {'c', 'o', 'l', 'o', 'r', 0}; for (unsigned i = start; i < limit; i++) { snprintf(buf + 5, sizeof(buf) - 6, "%u", i); PyObject *val = PyLong_FromUnsignedLong(self->color_table[i] &(~GENERATED_COLOR_MASK)); if (!val) return false; int ret = PyDict_SetItemString(ans, buf, val); Py_DECREF(val); if (ret != 0) return false; } return true; } static PyObject* basic_colors(ColorProfile *self, PyObject *args UNUSED) { #define basic_colors_doc "Return the basic colors as a dictionary of color_name to integer or None (names are the same as used in kitty.conf)" RAII_PyObject(ans, PyDict_New()); if (ans == NULL) return NULL; if (!colortable_colors_into_dict(self, 0, 16, ans)) return NULL; #define D(attr, name) { \ unsigned long c = colorprofile_to_color(self, self->overridden.attr, self->configured.attr).rgb; \ PyObject *val = PyLong_FromUnsignedLong(c); if (!val) return NULL; \ int ret = PyDict_SetItemString(ans, #name, val); Py_DECREF(val); \ if (ret != 0) return NULL; \ } D(default_fg, foreground); D(default_bg, background); #undef D return Py_NewRef(ans); } static PyObject* as_dict(ColorProfile *self, PyObject *args UNUSED) { #define as_dict_doc "Return all colors as a dictionary of color_name to integer or None (names are the same as used in kitty.conf)" RAII_PyObject(ans, PyDict_New()); if (ans == NULL) return NULL; if (!colortable_colors_into_dict(self, 0, arraysz(self->color_table), ans)) return NULL; #define D(attr, name) { \ if (self->overridden.attr.type != COLOR_NOT_SET) { \ int ret; PyObject *val; \ if (self->overridden.attr.type == COLOR_IS_SPECIAL) { \ val = Py_NewRef(Py_None); \ } else { \ unsigned long c = colorprofile_to_color(self, self->overridden.attr, self->configured.attr).rgb; \ val = PyLong_FromUnsignedLong(c); \ } \ if (!val) { return NULL; } \ ret = PyDict_SetItemString(ans, #name, val); \ Py_DECREF(val); \ if (ret != 0) { return NULL; } \ }} D(default_fg, foreground); D(default_bg, background); D(cursor_color, cursor); D(cursor_text_color, cursor_text); D(highlight_fg, selection_foreground); D(highlight_bg, selection_background); D(visual_bell_color, visual_bell_color); RAII_PyObject(transparent_background_colors, PyList_New(0)); if (!transparent_background_colors) return NULL; for (size_t i = 0; i < arraysz(self->overriden_transparent_colors); i++) { TransparentDynamicColor *c = NULL; if (self->overriden_transparent_colors[i].is_set) c = self->overriden_transparent_colors + i; else if (self->configured_transparent_colors[i].is_set) c = self->configured_transparent_colors + i; if (c) { RAII_PyObject(t, Py_BuildValue("Nf", alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, 0), c->opacity)); if (!t) return NULL; if (PyList_Append(transparent_background_colors, t) != 0) return NULL; } } if (PyList_GET_SIZE(transparent_background_colors)) { RAII_PyObject(t, PyList_AsTuple(transparent_background_colors)); if (!t) return NULL; if (PyDict_SetItemString(ans, "transparent_background_colors", t) != 0) return NULL; } #undef D return Py_NewRef(ans); } static PyObject* as_color(ColorProfile *self, PyObject *val) { #define as_color_doc "Convert the specified terminal color into an (r, g, b) tuple based on the current profile values" if (!PyLong_Check(val)) { PyErr_SetString(PyExc_TypeError, "val must be an int"); return NULL; } unsigned long entry = PyLong_AsUnsignedLong(val); unsigned int t = entry & 0xFF; uint8_t r; uint32_t col = 0; switch(t) { case 1: r = (entry >> 8) & 0xff; col = self->color_table[r] & (~GENERATED_COLOR_MASK); break; case 2: col = entry >> 8; break; default: Py_RETURN_NONE; } Color *ans = PyObject_New(Color, &Color_Type); if (ans) { ans->color.val = 0; ans->color.rgb = col; } return (PyObject*)ans; } static PyObject* reset_color_table(ColorProfile *self, PyObject *a UNUSED) { #define reset_color_table_doc "Reset all customized colors back to defaults" memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256)); self->dirty = true; Py_RETURN_NONE; } static PyObject* reset_color(ColorProfile *self, PyObject *val) { #define reset_color_doc "Reset the specified color" uint8_t i = PyLong_AsUnsignedLong(val) & 0xff; self->color_table[i] = self->orig_color_table[i]; self->dirty = true; Py_RETURN_NONE; } static PyObject* set_color(ColorProfile *self, PyObject *args) { #define set_color_doc "Set the specified color" unsigned char i; unsigned long val; if (!PyArg_ParseTuple(args, "Bk", &i, &val)) return NULL; self->dirty = true; if (val == NULL_COLOR_VALUE && i >= 16) { bool semantic, dynamic = palette_generation_is_dynamic(global_state.options_object, &semantic); self->color_table[i] = dynamic ? generate_256_palette_color(self, self->color_table, i, semantic) : GENERATED_COLOR_MASK | FG_BG_256[i]; } else self->color_table[i] = val; Py_RETURN_NONE; } void copy_color_table_to_buffer(ColorProfile *self, color_type *buf, int offset, size_t stride) { size_t i; stride = MAX(1u, stride); for (i = 0, buf = buf + offset; i < arraysz(self->color_table); i++, buf += stride) *buf = self->color_table[i] & (~GENERATED_COLOR_MASK); // Copy the mark colors for (i = 0; i < arraysz(self->mark_backgrounds); i++) { *buf = self->mark_backgrounds[i]; buf += stride; } for (i = 0; i < arraysz(self->mark_foregrounds); i++) { *buf = self->mark_foregrounds[i]; buf += stride; } self->dirty = false; } static void push_onto_color_stack_at(ColorProfile *self, unsigned int i) { self->color_stack[i].dynamic_colors = self->overridden; memcpy(self->color_stack[i].transparent_colors, self->overriden_transparent_colors, sizeof(self->overriden_transparent_colors)); self->color_stack[i].dynamic_colors = self->overridden; memcpy(self->color_stack[i].color_table, self->color_table, sizeof(self->color_stack->color_table)); } static void copy_from_color_stack_at(ColorProfile *self, unsigned int i) { self->overridden = self->color_stack[i].dynamic_colors; memcpy(self->color_table, self->color_stack[i].color_table, sizeof(self->color_table)); memcpy(self->overriden_transparent_colors, self->color_stack[i].transparent_colors, sizeof(self->overriden_transparent_colors)); } bool colorprofile_push_colors(ColorProfile *self, unsigned int idx) { if (idx > 10) return false; size_t sz = idx ? idx : self->color_stack_idx + 1; sz = MIN(10u, sz); if (self->color_stack_sz < sz) { self->color_stack = realloc(self->color_stack, sz * sizeof(self->color_stack[0])); if (self->color_stack == NULL) fatal("Out of memory while ensuring space for %zu elements in color stack", sz); memset(self->color_stack + self->color_stack_sz, 0, (sz - self->color_stack_sz) * sizeof(self->color_stack[0])); self->color_stack_sz = sz; } if (idx == 0) { if (self->color_stack_idx >= self->color_stack_sz) { memmove(self->color_stack, self->color_stack + 1, (self->color_stack_sz - 1) * sizeof(self->color_stack[0])); idx = self->color_stack_sz - 1; } else idx = self->color_stack_idx++; push_onto_color_stack_at(self, idx); return true; } idx -= 1; if (idx < self->color_stack_sz) { push_onto_color_stack_at(self, idx); return true; } return false; } void colorprofile_reset(ColorProfile *self) { memcpy(self->color_table, self->orig_color_table, sizeof(FG_BG_256)); self->dirty = true; self->color_stack_idx = 0; zero_at_ptr(&self->overridden); for (unsigned i = 0; i < arraysz(self->overriden_transparent_colors); i++) { zero_at_ptr(self->overriden_transparent_colors + i); } for (unsigned i = 0; i < self->color_stack_sz; i++) { zero_at_ptr(self->color_stack + i); } } bool colorprofile_pop_colors(ColorProfile *self, unsigned int idx) { if (idx == 0) { if (!self->color_stack_idx) return false; copy_from_color_stack_at(self, --self->color_stack_idx); memset(self->color_stack + self->color_stack_idx, 0, sizeof(self->color_stack[0])); return true; } idx -= 1; if (idx < self->color_stack_sz) { copy_from_color_stack_at(self, idx); return true; } return false; } void colorprofile_report_stack(ColorProfile *self, unsigned int *idx, unsigned int *count) { *count = self->color_stack_idx; *idx = self->color_stack_idx ? self->color_stack_idx - 1 : 0; } static PyObject* color_table_address(ColorProfile *self, PyObject *a UNUSED) { #define color_table_address_doc "Pointer address to start of color table" return PyLong_FromVoidPtr((void*)self->color_table); } static PyObject* default_color_table(PyObject *self UNUSED, PyObject *args UNUSED) { return create_256_color_table(); } // Boilerplate {{{ #define CGETSET(name, nullable) \ static PyObject* name##_get(ColorProfile *self, void UNUSED *closure) { \ DynamicColor ans = colorprofile_to_color(self, self->overridden.name, self->configured.name); \ if (ans.type == COLOR_IS_SPECIAL) { \ if (nullable) Py_RETURN_NONE; \ return (PyObject*)alloc_color(0, 0, 0, 0); \ } \ return (PyObject*)alloc_color((ans.rgb >> 16) & 0xff, (ans.rgb >> 8) & 0xff, ans.rgb & 0xff, 0); \ } \ static int name##_set(ColorProfile *self, PyObject *v, void UNUSED *closure) { \ if (v == NULL) { self->overridden.name.val = 0; return 0; } \ if (PyLong_Check(v)) { \ unsigned long val = PyLong_AsUnsignedLong(v); \ self->overridden.name.rgb = val & 0xffffff; \ self->overridden.name.type = COLOR_IS_RGB; \ } else if (PyObject_TypeCheck(v, &Color_Type)) { \ Color *c = (Color*)v; self->overridden.name.rgb = c->color.rgb; self->overridden.name.type = COLOR_IS_RGB; \ } else if (v == Py_None) { \ if (!nullable) { PyErr_SetString(PyExc_TypeError, #name " cannot be set to None"); return -1; } \ self->overridden.name.type = COLOR_IS_SPECIAL; self->overridden.name.rgb = 0; \ } \ self->dirty = true; return 0; \ } CGETSET(default_fg, false) CGETSET(default_bg, false) CGETSET(cursor_color, true) CGETSET(cursor_text_color, true) CGETSET(highlight_fg, true) CGETSET(highlight_bg, true) CGETSET(visual_bell_color, true) #undef CGETSET static PyGetSetDef cp_getsetters[] = { GETSET(default_fg) GETSET(default_bg) GETSET(cursor_color) GETSET(cursor_text_color) GETSET(highlight_fg) GETSET(highlight_bg) GETSET(visual_bell_color) {NULL} /* Sentinel */ }; static PyMemberDef cp_members[] = { {NULL} }; static PyObject* palette_color_is_generated(ColorProfile *self, PyObject *x) { if (!PyLong_Check(x)) { PyErr_SetString(PyExc_TypeError, "idx must be an integer"); return NULL; } size_t idx = PyLong_AsUnsignedLong(x); if (idx >= arraysz(FG_BG_256)) { PyErr_SetString(PyExc_IndexError, "idx out of bounds"); return NULL; } return Py_NewRef(is_generated_color(self->color_table[idx]) ? Py_True : Py_False); } static PyObject* reload_from_opts(ColorProfile *self, PyObject *args, PyObject *kw) { PyObject *opts = NULL; int reset_overriden_colors = 1; static const char* kwlist[] = {"opts", "reset_overriden_colors", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kw, "|Op", (char**)kwlist, &opts, &reset_overriden_colors)) return NULL; if (opts == NULL) opts = global_state.options_object; self->dirty = true; if (!set_configured_colors(self, opts)) return NULL; if (reset_overriden_colors) { zero_at_ptr(&self->overridden); memset(self->overriden_transparent_colors, 0, sizeof(self->overriden_transparent_colors)); } if (!set_mark_colors(self, opts)) return NULL; if (!set_colortable(self, opts)) return NULL; Py_RETURN_NONE; } static PyObject* get_transparent_background_color(ColorProfile *self, PyObject *index) { if (!PyLong_Check(index)) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; } unsigned long idx = PyLong_AsUnsignedLong(index); if (PyErr_Occurred()) return NULL; if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE; TransparentDynamicColor *c = self->overriden_transparent_colors[idx].is_set ? self->overriden_transparent_colors + idx : self->configured_transparent_colors + idx; if (!c->is_set) Py_RETURN_NONE; float opacity = c->opacity >= 0 ? c->opacity : OPT(background_opacity); return (PyObject*)alloc_color((c->color >> 16) & 0xff, (c->color >> 8) & 0xff, c->color & 0xff, (unsigned)(255.f * opacity)); } static PyObject* set_transparent_background_color(ColorProfile *self, PyObject *const *args, Py_ssize_t nargs) { if (nargs < 1) { PyErr_SetString(PyExc_TypeError, "must specify index"); return NULL; } if (!PyLong_Check(args[0])) { PyErr_SetString(PyExc_TypeError, "index must be an int"); return NULL; } unsigned long idx = PyLong_AsUnsignedLong(args[0]); if (PyErr_Occurred()) return NULL; if (idx >= arraysz(self->configured_transparent_colors)) Py_RETURN_NONE; if (nargs < 2) { self->overriden_transparent_colors[idx].is_set = false; Py_RETURN_NONE; } if (!PyObject_TypeCheck(args[1], &Color_Type)) { PyErr_SetString(PyExc_TypeError, "color must be Color object"); return NULL; } Color *c = (Color*)args[1]; float opacity = (float)(c->color.alpha) / 255.f; if (nargs > 2 && PyFloat_Check(args[2])) opacity = (float)PyFloat_AsDouble(args[2]); self->overriden_transparent_colors[idx].is_set = true; self->overriden_transparent_colors[idx].color = c->color.rgb; self->overriden_transparent_colors[idx].opacity = MAX(-1.f, MIN(opacity, 1.f)); Py_RETURN_NONE; } static PyMethodDef cp_methods[] = { METHOD(reset_color_table, METH_NOARGS) METHOD(as_dict, METH_NOARGS) METHOD(basic_colors, METH_NOARGS) METHOD(color_table_address, METH_NOARGS) METHOD(as_color, METH_O) METHOD(reset_color, METH_O) METHOD(set_color, METH_VARARGS) METHODB(palette_color_is_generated, METH_O), METHODB(get_transparent_background_color, METH_O), {"reload_from_opts", (PyCFunction)(void (*) (void))(reload_from_opts), METH_VARARGS | METH_KEYWORDS, NULL}, {"set_transparent_background_color", (PyCFunction)(void(*)(void))set_transparent_background_color, METH_FASTCALL, ""}, {NULL} /* Sentinel */ }; PyTypeObject ColorProfile_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "fast_data_types.ColorProfile", .tp_basicsize = sizeof(ColorProfile), .tp_dealloc = (destructor)dealloc_cp, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = "ColorProfile", .tp_members = cp_members, .tp_methods = cp_methods, .tp_getset = cp_getsetters, .tp_new = new_cp, }; // }}} static Color* alloc_color(unsigned char r, unsigned char g, unsigned char b, unsigned a) { Color *self = (Color *)(&Color_Type)->tp_alloc(&Color_Type, 0); if (self != NULL) { self->color.r = r; self->color.g = g; self->color.b = b; self->color.a = a; } return self; } static PyObject * new_color(PyTypeObject *type UNUSED, PyObject *args, PyObject *kwds) { static const char* kwlist[] = {"red", "green", "blue", "alpha", NULL}; unsigned char r = 0, g = 0, b = 0, a = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|BBBB", (char**)kwlist, &r, &g, &b, &a)) return NULL; return (PyObject*) alloc_color(r, g, b, a); } static PyObject * color_vectorcall(PyObject *type UNUSED, PyObject *const *args, size_t nargsf, PyObject *kwnames) { const Py_ssize_t nargs = PyVectorcall_NARGS(nargsf); if (nargs > 4) { PyErr_SetString(PyExc_TypeError, "Color() takes at most 4 arguments"); return NULL; } unsigned char rgba[4] = {0, 0, 0, 0}; for (Py_ssize_t i = 0; i < nargs; i++) { unsigned long val = PyLong_AsUnsignedLongMask(args[i]); if (val == (unsigned long)-1 && PyErr_Occurred()) return NULL; rgba[i] = (unsigned char)val; } if (kwnames) { const unsigned num = PyTuple_GET_SIZE(kwnames); for (unsigned i = 0; i < num; i++) { const char *name = PyUnicode_AsUTF8(PyTuple_GET_ITEM(kwnames, i)); if (!name) return NULL; int idx; #define C(ch, i, expected) case ch: idx = i; if (strcmp(name, expected) != 0) { \ PyErr_Format(PyExc_TypeError, "Color() got an unexpected keyword argument '%s'", name); return NULL; }; break; switch(name[0]) { C('r', 0, "red"); C('g', 1, "green"); C('b', 2, "blue"); C('a', 3, "alpha"); default: PyErr_Format(PyExc_TypeError, "Color() got an unexpected keyword argument '%s'", name); return NULL; } #undef C if (idx < nargs) { PyErr_Format(PyExc_TypeError, "Color() got multiple values for argument '%s'", name); return NULL; } unsigned long val = PyLong_AsUnsignedLongMask(args[nargs + i]); if (val == (unsigned long)-1 && PyErr_Occurred()) return NULL; rgba[idx] = (unsigned char)val; } } return (PyObject*)alloc_color(rgba[0], rgba[1], rgba[2], rgba[3]); } static PyObject* Color_as_int(Color *self) { return PyLong_FromUnsignedLong(self->color.val); } static PyObject* color_truediv(Color *self, PyObject *divisor) { RAII_PyObject(o, PyNumber_Float(divisor)); if (o == NULL) return NULL; double r = self->color.r, g = self->color.g, b = self->color.b, a = self->color.a; double d = PyFloat_AS_DOUBLE(o) * 255.; return Py_BuildValue("dddd", r/d, g/d, b/d, a/d); } static PyNumberMethods color_number_methods = { .nb_int = (unaryfunc)Color_as_int, .nb_true_divide = (binaryfunc)color_truediv, }; #define CGETSET(name) \ static PyObject* name##_get(Color *self, void UNUSED *closure) { return PyLong_FromUnsignedLong(self->color.name); } CGETSET(red) CGETSET(green) CGETSET(blue) CGETSET(alpha) #undef CGETSET static PyObject* rgb_get(Color *self, void *closure UNUSED) { return PyLong_FromUnsignedLong(self->color.rgb); } static PyObject* luminance_get(Color *self, void *closure UNUSED) { return PyFloat_FromDouble(rgb_luminance(self->color) / 255.0); } static PyObject* is_dark_get(Color *self, void *closure UNUSED) { if (rgb_luminance(self->color) / 255.0 < 0.5) Py_RETURN_TRUE; Py_RETURN_FALSE; } static PyObject* sgr_get(Color* self, void *closure UNUSED) { char buf[32]; int sz = snprintf(buf, sizeof(buf), ":2:%u:%u:%u", self->color.r, self->color.g, self->color.b); return PyUnicode_FromStringAndSize(buf, sz); } static PyObject* sharp_get(Color* self, void *closure UNUSED) { char buf[32]; int sz; if (self->color.alpha) sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x%02x", self->color.a, self->color.r, self->color.g, self->color.b); else sz = snprintf(buf, sizeof(buf), "#%02x%02x%02x", self->color.r, self->color.g, self->color.b); return PyUnicode_FromStringAndSize(buf, sz); } static PyObject* color_cmp(PyObject *self, PyObject *other, int op) { if (op != Py_EQ && op != Py_NE) return Py_NotImplemented; if (!PyObject_TypeCheck(other, &Color_Type)) { if (op == Py_EQ) Py_RETURN_FALSE; Py_RETURN_TRUE; } Color *a = (Color*)self, *b = (Color*)other; switch (op) { case Py_EQ: { if (a->color.val == b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } case Py_NE: { if (a->color.val != b->color.val) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } default: return Py_NotImplemented; } } static PyGetSetDef color_getsetters[] = { {"rgb", (getter) rgb_get, NULL, "rgb", NULL}, {"red", (getter) red_get, NULL, "red", NULL}, {"green", (getter) green_get, NULL, "green", NULL}, {"blue", (getter) blue_get, NULL, "blue", NULL}, {"alpha", (getter) alpha_get, NULL, "alpha", NULL}, {"r", (getter) red_get, NULL, "red", NULL}, {"g", (getter) green_get, NULL, "green", NULL}, {"b", (getter) blue_get, NULL, "blue", NULL}, {"a", (getter) alpha_get, NULL, "alpha", NULL}, {"luminance", (getter) luminance_get, NULL, "luminance", NULL}, {"as_sgr", (getter) sgr_get, NULL, "as_sgr", NULL}, {"as_sharp", (getter) sharp_get, NULL, "as_sharp", NULL}, {"is_dark", (getter) is_dark_get, NULL, "is_dark", NULL}, {NULL} /* Sentinel */ }; static PyObject* contrast(Color* self, PyObject *o) { if (!PyObject_TypeCheck(o, &Color_Type)) { PyErr_SetString(PyExc_TypeError, "Not a Color"); return NULL; } Color *other = (Color*) o; return PyFloat_FromDouble(rgb_contrast(self->color, other->color)); } static int hexchar_to_int(char c) { switch (c) { START_ALLOW_CASE_RANGE case '0' ... '9': return c - '0'; case 'a' ... 'f': return c - 'a' + 10; case 'A' ... 'F': return c - 'A' + 10; END_ALLOW_CASE_RANGE } return -1; } static bool parse_base16_uchar(const char *hex, unsigned char *out) { const int hi = hexchar_to_int(hex[0]); const int lo = hexchar_to_int(hex[1]); if (hi < 0 || lo < 0) return false; *out = (unsigned char)((hi << 4) | lo); return true; } static bool parse_double(const char *src, double *out) { char *endptr; errno = 0; *out = strtod_l(src, &endptr, get_c_locale()); return endptr != src && *endptr == 0 && errno == 0; } static bool parse_single_color(const char *c, size_t len, unsigned char *out) { char buf[2]; if (len == 1) { buf[0] = c[0]; buf[1] = c[0]; c = buf; } return parse_base16_uchar(c, out); } static PyObject* parse_sharp(const char *spec, size_t len) { unsigned char r, g, b; switch(len) { case 3: if (!parse_single_color(spec, 1, &r) || !parse_single_color(spec + 1, 1, &g) || !parse_single_color(spec + 2, 1, &b)) Py_RETURN_NONE; break; case 6: case 9: case 12: if (!parse_single_color(spec, 2, &r) || !parse_single_color(spec + len/3, 2, &g) || !parse_single_color(spec + 2 * len / 3, 2, &b)) Py_RETURN_NONE; break; default: Py_RETURN_NONE; } return (PyObject*)alloc_color(r, g, b, 0); } static PyObject* parse_rgb(const char *spec, size_t len) { char buf[32]; if (len >= sizeof(buf)) Py_RETURN_NONE; memcpy(buf, spec, len); buf[len] = 0; unsigned char r, g, b; char *tok; #define p(buf, out) if (!(tok = strtok(buf, "/")) || !parse_single_color(tok, strlen(tok), &out)) Py_RETURN_NONE; p(buf, r); p(NULL, g); p(NULL, b); #undef p return (PyObject*)alloc_color(r, g, b, 0); } static unsigned char as8bit(double f) { return (unsigned char)(round(f * 255.)); } static bool parse_single_intensity(const char *s, unsigned char *out) { double f; if (!parse_double(s, &f)) return false; *out = as8bit(f); return true; } static PyObject* parse_rgbi(const char *spec, size_t len) { char buf[256]; if (len >= sizeof(buf)) Py_RETURN_NONE; memcpy(buf, spec, len); buf[len] = 0; unsigned char r, g, b; char *tok; #define p(buf, out) if (!(tok = strtok(buf, "/")) || !parse_single_intensity(tok, &out)) Py_RETURN_NONE; p(buf, r); p(NULL, g); p(NULL, b); #undef p return (PyObject*)alloc_color(r, g, b, 0); } static bool parse_double_intensity(char *s, double *out, double percentage_divider) { size_t l = strlen(s); if (l == 0) return false; double divisor = 1; if (s[l-1] == '%') { s[l-1] = 0; divisor = percentage_divider; } if (!parse_double(s, out)) return false; *out /= divisor; return true; } static double clamp(const double f) { return MAX(0, MIN(f, 1)); } static double linear_to_srgb(double c) { return c <= 0.0031308 ? c * 12.92 : (1.055 * pow(c, (1 / 2.4)) - 0.055); } static double degrees_to_radians(double degrees) { return degrees * (M_PI / 180); } static double radians_to_degrees(double radians) { return 180 * radians / M_PI; } static void oklch_to_srgb(double l, double c, double h, double *r, double *g, double *b) { // Convert OKLCH to OKLab const double h_rad = degrees_to_radians(h); const double a = c * cos(h_rad); const double lb = c * sin(h_rad); // Convert OKLab to Linear sRGB // Using the OKLab to Linear sRGB transformation const double l_ = l + 0.3963377774 * a + 0.2158037573 * lb; const double m_ = l - 0.1055613458 * a - 0.0638541728 * lb; const double s_ = l - 0.0894841775 * a - 1.2914855480 * lb; const double l_lin = l_ * l_ * l_; const double m_lin = m_ * m_ * m_; const double s_lin = s_ * s_ * s_; const double r_lin = +4.0767416621 * l_lin - 3.3077115913 * m_lin + 0.2309699292 * s_lin; const double g_lin = -1.2684380046 * l_lin + 2.6097574011 * m_lin - 0.3413193965 * s_lin; const double b_lin = -0.0041960863 * l_lin - 0.7034186147 * m_lin + 1.7076147010 * s_lin; *r = linear_to_srgb(clamp(r_lin)); *g = linear_to_srgb(clamp(g_lin)); *b = linear_to_srgb(clamp(b_lin)); } static double srgb_to_linear(double c) { return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4); } static void srgb_to_oklab(double r, double g, double b, double *l, double *a, double *lb) { // Convert sRGB to linear sRGB const double r_lin = srgb_to_linear(r); const double g_lin = srgb_to_linear(g); const double b_lin = srgb_to_linear(b); // Convert Linear sRGB to OKLab (inverse of oklch_to_srgb) const double l_lin = 0.4122214708 * r_lin + 0.5363325363 * g_lin + 0.0514459929 * b_lin; const double m_lin = 0.2119034982 * r_lin + 0.6806995451 * g_lin + 0.1073969566 * b_lin; const double s_lin = 0.0883024619 * r_lin + 0.2817188376 * g_lin + 0.6299787005 * b_lin; const double l_ = l_lin != 0 ? copysign(pow(fabs(l_lin), 1./3.), l_lin) : 0; const double m_ = m_lin != 0 ? copysign(pow(fabs(m_lin), 1./3.), m_lin) : 0; const double s_ = s_lin != 0 ? copysign(pow(fabs(s_lin), 1./3.), s_lin) : 0; // OKLab coordinates *l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_; *a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_; *lb = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_; } static double distance(double x_l, double x_a, double x_b, double y_l, double y_a, double y_b) { return sqrt((x_l - y_l)*(x_l - y_l) + (x_a - y_a)*(x_a - y_a) + (x_b - y_b)*(x_b - y_b)); } static void oklch_to_srgb_gamut_map(double l, double c, double h, double *r, double *g, double *b) { // Edge cases: pure black or white don't need gamut mapping if (!isfinite(l) || !isfinite(c) || !isfinite(h) || l <= 0) { *r = 0; *g = 0; *b = 0; return; } if (l >= 1) { *r = 1; *g = 1; *b = 1; return; } // Constants from CSS Color Module Level 4 static const double JND = 0.02; // Just Noticeable Difference threshold (2% in deltaEOK) static const double MIN_CONVERGENCE = 0.0001; // Binary search precision (0.01% chroma) static const double EPSILON = 0.00001; // Small value for doubleing point comparisons // If chroma is very small, color is essentially achromatic if (c < EPSILON) { *r = linear_to_srgb(l); *g = *r; *b = *r; return; } // Try the original color first oklch_to_srgb(l, c, h, r, g, b); #define in_gamut(r,g,b) (0. <= r && r <= 1. && 0. <= g && g <= 1. && 0. <= b && b <= 1.) if (in_gamut(*r,*g,*b)) return; // Binary search for maximum in-gamut chroma double low_chroma = 0, high_chroma = c, r_test, g_test, b_test, r_clipped, g_clipped, b_clipped; // Convert original color to OKLab for deltaE calculations while ((high_chroma - low_chroma) > MIN_CONVERGENCE) { double mid_chroma = (high_chroma + low_chroma) * 0.5; // Try this chroma value oklch_to_srgb(l, mid_chroma, h, &r_test, &g_test, &b_test); // Check if in gamut (before clipping) if (in_gamut(r_test, g_test, b_test)) { // In gamut - try higher chroma low_chroma = mid_chroma; } else { // Out of gamut - clip and check deltaE r_clipped = clamp(r_test); g_clipped = clamp(g_test); b_clipped = clamp(b_test); // Convert both to OKLab for comparison double l_test, a_test, lb_test, l_clipped, a_clipped, lb_clipped; srgb_to_oklab(r_test, g_test, b_test, &l_test, &a_test, &lb_test); srgb_to_oklab(r_clipped, g_clipped, b_clipped, &l_clipped, &a_clipped, &lb_clipped); // Calculate perceptual difference double de = distance(l_test, a_test, lb_test, l_clipped, a_clipped, lb_clipped); if (de < JND) { // Difference is imperceptible - accept this chroma low_chroma = mid_chroma; } else { // Difference is noticeable - reduce chroma more high_chroma = mid_chroma; } } } // Use the final chroma value and clip to ensure in-gamut oklch_to_srgb(l, low_chroma, h, r, g, b); *r = clamp(*r); *g = clamp(*g); *b = clamp(*b); #undef in_gamut } static double f_inv(double t) { static const double delta = 6. / 29.; return t > delta ? t*t*t : 3 * delta * delta * (t - 4. / 29.); } static void lab_to_oklch(double l, double a, double b, double *okl, double *c, double *h) { const double y = (l + 16.) / 116.; const double x = a / 500. + y; const double z = y - b / 200.; const double x_val = 0.95047 * f_inv(x); const double y_val = f_inv(y); const double z_val = 1.08883 * f_inv(z); // XYZ to Linear sRGB (don't clip here to preserve out-of-gamut info) const double r_lin = +3.2404542 * x_val - 1.5371385 * y_val - 0.4985314 * z_val; const double g_lin = -0.9692660 * x_val + 1.8760108 * y_val + 0.0415560 * z_val; const double b_lin = +0.0556434 * x_val - 0.2040259 * y_val + 1.0572252 * z_val; // Convert linear sRGB to sRGB gamma const double r_srgb = r_lin >= 0 ? linear_to_srgb(r_lin) : 0; const double g_srgb = g_lin >= 0 ? linear_to_srgb(g_lin) : 0; const double b_srgb = b_lin >= 0 ? linear_to_srgb(b_lin) : 0; // Convert to OKLab double a_ok, b_ok; srgb_to_oklab(r_srgb, g_srgb, b_srgb, okl, &a_ok, &b_ok); // Convert OKLab to OKLCH *c = sqrt(a_ok * a_ok + b_ok * b_ok); *h = fmod(radians_to_degrees(atan2(b_ok, a_ok)), 360.f); } static PyObject* parse_oklch(const char *spec, size_t len) { if (len < 10 || spec[--len] != ')') Py_RETURN_NONE; if (spec[0] != 'k' || spec[1] != 'l' || spec[2] != 'c' || spec[3] != 'h' || spec[4] != '(') Py_RETURN_NONE; spec += 5; len -= 5; char buf[256]; if (len >= sizeof(buf)) Py_RETURN_NONE; memcpy(buf, spec, len); buf[len] = 0; double l, c, h; char *tok; #define p(buf, out) if (!(tok = strtok(buf, " ,")) || !parse_double_intensity(tok, &out, 100)) Py_RETURN_NONE; p(buf, l); p(NULL, c); p(NULL, h); #undef p // Clamp to reasonable ranges l = clamp(l); c = MAX(0.f, c); // Chroma is unbounded but we don't clamp high end h = fmod(h, 360); // Wrap hue to 0-360 double r, g, b; oklch_to_srgb_gamut_map(l, c, h, &r, &g, &b); return (PyObject*)alloc_color(as8bit(r), as8bit(g), as8bit(b), 0); } static PyObject* parse_lab(const char *spec, size_t len) { if (len < 8 || spec[--len] != ')') Py_RETURN_NONE; if (spec[0] != 'a' || spec[1] != 'b' || spec[2] != '(') Py_RETURN_NONE; spec += 3; len -= 3; char buf[256]; if (len >= sizeof(buf)) Py_RETURN_NONE; memcpy(buf, spec, len); buf[len] = 0; double l, a, b; char *tok; #define p(buf, out) if (!(tok = strtok(buf, " ,")) || !parse_double_intensity(tok, &out, 1)) Py_RETURN_NONE; p(buf, l); p(NULL, a); p(NULL, b); #undef p // Clamp to reasonable ranges double okl, c, h, r, g, bb; lab_to_oklch(MAX(0., MIN(l, 100.)), a, b, &okl, &c, &h); oklch_to_srgb_gamut_map(okl, c, h, &r, &g, &bb); return (PyObject*)alloc_color(as8bit(r), as8bit(g), as8bit(bb), 0); } static const char* trim_view(const char *str, Py_ssize_t *len) { if (str == NULL || *len == 0) return str; const char *start = str; const char *end = str + *len - 1; while (start <= end && isspace((unsigned char)*start)) start++; while (end > start && isspace((unsigned char)*end)) end--; if (start > end) { *len = 0; } else { *len = (size_t)(end - start + 1); } return start; } static PyObject* parse_color(PyTypeObject *type UNUSED, PyObject *pspec) { if (!PyUnicode_Check(pspec)) { PyErr_SetString(PyExc_TypeError, "spec must be a string"); return NULL; } RAII_PyObject(lower_cased, PyObject_CallMethod(pspec, "lower", NULL)); if (!lower_cased) return NULL; Py_ssize_t len; const char *spec = PyUnicode_AsUTF8AndSize(lower_cased, &len); spec = trim_view(spec, &len); if (len < 2) Py_RETURN_NONE; // Remove trailing comments switch (spec[0]) { case '#': { const char *s = strchr(spec, ' '); if (s) len = s - spec; } break; default: { const char *s = strchr(spec, '#'); if (s) { len = s - spec; spec = trim_view(spec, &len); } } break; } const struct Keyword *k = in_color_name_set(spec, len); if (k) return (PyObject*)alloc_color((k->value >> 16) & 0xff, (k->value >> 8) & 0xff, k->value & 0xff, 0); if (len < 4) Py_RETURN_NONE; switch (spec[0]) { case '#': return parse_sharp(spec + 1, len - 1); case 'r': if (spec[1] != 'g' || spec[2] != 'b' || len < 6) Py_RETURN_NONE; switch(spec[3]) { case ':': return parse_rgb(spec + 4, len - 4); case 'i': if (spec[4] == 'i' && spec[5] == ':') return parse_rgbi(spec + 5, len - 5); } Py_RETURN_NONE; case 'o': return parse_oklch(spec + 1, len - 1); case 'l': return parse_lab(spec + 1, len - 1); } Py_RETURN_NONE; } static PyMethodDef color_methods[] = { METHODB(contrast, METH_O), METHODB(parse_color, METH_O | METH_CLASS), {NULL} /* Sentinel */ }; static PyObject * repr(Color *self) { if (self->color.alpha) return PyUnicode_FromFormat("Color(red=%u, green=%u, blue=%u, alpha=%u)", self->color.r, self->color.g, self->color.b, self->color.a); return PyUnicode_FromFormat("Color(%u, %u, %u)", self->color.r, self->color.g, self->color.b); } static Py_hash_t color_hash(PyObject *x) { return ((Color*)x)->color.val; } PyTypeObject Color_Type = { PyVarObject_HEAD_INIT(NULL, 0) .tp_name = "kitty.fast_data_types.Color", .tp_basicsize = sizeof(Color), .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_VECTORCALL, .tp_doc = "Color", .tp_new = new_color, .tp_vectorcall = color_vectorcall, .tp_getset = color_getsetters, .tp_as_number = &color_number_methods, .tp_methods = color_methods, .tp_repr = (reprfunc)repr, .tp_hash = color_hash, .tp_richcompare = color_cmp, }; static PyObject* all_color_names(PyObject *self UNUSED, PyObject *args UNUSED) { RAII_PyObject(ans, PyTuple_New(TOTAL_KEYWORDS)); if (!ans) return NULL; const struct Keyword *k; Py_ssize_t n = 0; for (unsigned i = 0; i <= MAX_HASH_VALUE; i++) { if ((k = &color_names[i])->name > -1) { const char *name = color_names[i].name + stringpool; PyObject *t = Py_BuildValue("sN", name, alloc_color((k->value >> 16) & 0xff, (k->value >> 8) & 0xff, k->value & 0xff, 0)); if (!t) return NULL; PyTuple_SET_ITEM(ans, n, t); n++; } } return Py_NewRef(ans); } static PyMethodDef module_methods[] = { METHODB(default_color_table, METH_NOARGS), METHODB(patch_color_profiles, METH_VARARGS), METHODB(all_color_names, METH_NOARGS), {NULL, NULL, 0, NULL} /* Sentinel */ }; int init_ColorProfile(PyObject *module) {\ if (PyType_Ready(&ColorProfile_Type) < 0) return 0; if (PyModule_AddObject(module, "ColorProfile", (PyObject *)&ColorProfile_Type) != 0) return 0; Py_INCREF(&ColorProfile_Type); if (PyType_Ready(&Color_Type) < 0) return 0; if (PyModule_AddObject(module, "Color", (PyObject *)&Color_Type) != 0) return 0; Py_INCREF(&Color_Type); if (PyModule_AddFunctions(module, module_methods) != 0) return false; PyModule_AddIntMacro(module, NULL_COLOR_VALUE); return 1; } // }}}